statsmodelsでSVARモデルの推定

前回に続いてSVARモデル(構造VARモデル)の話です。前回はモデルの数式の形を紹介しただけだったので、今回は実際に仮想的なデータを作ってみてPythonのコードで推定してみます。

サンプルとして、以下のようなモデルを考えました。

$${\scriptsize
\left[\begin{matrix}1&0&0\\0.3&1&0\\-0.2&-0.3&1\end{matrix}\right]\mathbf{y_t}=
\left[\begin{matrix}0.8\\-0.5\\0.3\end{matrix}\right]+
\left[\begin{matrix}-0.1&0.3&0.4\\0.2&-0.2&-0.3\\-0.3&0.4&0.2\end{matrix}\right]\mathbf{y_{t-1}}+
\left[\begin{matrix}0.1&-0.2&0.5\\-0.4&0.1&-0.4\\0.1&0.3&-0.2\end{matrix}\right]\mathbf{y_{t-2}}+
\boldsymbol{\varepsilon_t}
}$$
$${\scriptsize
\boldsymbol{\varepsilon}_t\sim W.N.\left(\left[\begin{matrix}0.2^2&0&0\\0&0.2^2&0\\0&0&0.2^2\end{matrix}\right]\right)
}$$

とりあえずこのモデルに従うサンプルデータを作らないといけないですね。

まず、各係数行列や定数項などを変数として格納しておきます。

import numpy as np
import pandas as pd


# 各係数を変数に格納
A = np.array(
    [
        [1, 0, 0],
        [0.3, 1, 0],
        [-0.2, -0.3, 1]
    ]
)
A1 = np.array(
    [
        [-0.1, 0.3, 0.4],
        [0.2, -0.2, -0.3],
        [-0.3, 0.4, 0.2]
    ]
)
A2 = np.array(
    [
        [0.1, -0.2, 0.5],
        [-0.4, 0.1, -0.4],
        [0.1, 0.3, -0.2]
    ]
)
c = np.array([0.8, -0.5, 0.3])

これを使って、データを作ります。行列$A$の逆行列を両辺にかけて、モデルを誘導形に変形して順番に計算したらOKです。最初の2時点のデータは適当に設定して、初期のデータを捨ててます。

# 最初の2点のデータy_0, y_1を仮置き
df = pd.DataFrame(
    {
        "y0": [1, 1],
        "y1": [1, 1],
        "y2": [1, 1],
    }
)

# Aの逆行列
A_inv = np.linalg.inv(A)

# 乱数固定
np.random.seed(1)
for i in range(len(df), 550):
    df.loc[i] = A_inv@(c+A1@df.iloc[-1].T+A2@df.iloc[-2].T+np.random.normal(size=3)*0.2)

# 最初の方のデータを切り捨てる。
df = df.iloc[50:]
df.reset_index(inplace=True, drop=True)

これでデータができました。グラフは省略していますが、plotしてみると定常であることがわかります。

データができたので、statsmodelsで推定してみましょう。ドキュメントはこちらです。
参考: statsmodels.tsa.vector_ar.svar_model.SVAR — statsmodels
推定結果についてはこちら。
参考: statsmodels.tsa.vector_ar.svar_model.SVARResults — statsmodels

これの使い方は結構特殊です。左辺の行列のうち、推定したいパラメーターを文字列”E”とした行列を作って一緒に渡してあげる必要があります。対角成分は1です。前回の記事で書きましたが、各過程が外生性が高い順に並んでると仮定しているんので、下三角行列になるようにしています。具体的には次のようなコードになります。

import statsmodels.api as sm


# 上式のAの中で求めたい要素を"E"とした行列を生成しモデルに渡す。
A_param = np.array([[1, 0, 0], ["E", 1, 0], ["E", "E", 1]])

svar_model = sm.tsa.SVAR(df, svar_type="A", A=A_param)
svar_result = svar_model.fit(maxlags=2)

はい、これでできました。推定されたパラメーターが変数svar_resultに入っていますので、順番に見ていきましょう。

# 左辺の行列A
print(svar_result.A)
"""
[[ 1.          0.          0.        ]
 [ 0.34089688  1.          0.        ]
 [-0.20440697 -0.31127542  1.        ]]
"""

# 右辺の係数はcoefsにまとめて入っている
print(svar_result.coefs)
"""
[[[-0.12822445  0.30295997  0.43086499]
  [ 0.28516025 -0.30381663 -0.40460447]
  [-0.25660898  0.38056812  0.17305955]]

 [[ 0.05884724 -0.18589909  0.52278365]
  [-0.40304548  0.11406067 -0.60635189]
  [-0.07409054  0.28827388 -0.23204729]]]
"""

# 定数項
print(svar_result.intercept)
"""
[ 0.86592879 -0.82006429  0.28888304]
"""

そこそこの精度で推定できていますね。

VARモデルの時と同様に、summary()メソッドで結果を一覧表示することもできます。ただ、statsmodelsの現在のバージョン(0.13.5)のバグだと思うのですが、k_exog_userってプロパティを手動で設定しておかないと、AttributeError: ‘SVARResults’ object has no attribute ‘k_exog_user’ってエラーが出ます。とりあえず0か何か突っ込んで実行しましょう。

svar_result.k_exog_user=0
svar_result.summary()
"""
  Summary of Regression Results   
==================================
Model:                        SVAR
Method:                        OLS
Date:           Mon, 27, Feb, 2023
Time:                     00:29:40
--------------------------------------------------------------------
No. of Equations:         3.00000    BIC:                   -9.36310
Nobs:                     498.000    HQIC:                  -9.47097
Log likelihood:           276.728    FPE:                7.18704e-05
AIC:                     -9.54065    Det(Omega_mle):     6.89230e-05
--------------------------------------------------------------------
Results for equation y0
========================================================================
           coefficient       std. error           t-stat            prob
------------------------------------------------------------------------
const         0.865929         0.038253           22.637           0.000
L1.y0        -0.128224         0.042999           -2.982           0.003
L1.y1         0.302960         0.037971            7.979           0.000
L1.y2         0.430865         0.041409           10.405           0.000
L2.y0         0.058847         0.043158            1.364           0.173
L2.y1        -0.185899         0.040139           -4.631           0.000
L2.y2         0.522784         0.035344           14.791           0.000
========================================================================

Results for equation y1
========================================================================
           coefficient       std. error           t-stat            prob
------------------------------------------------------------------------
const        -0.820064         0.043394          -18.898           0.000
L1.y0         0.285160         0.048778            5.846           0.000
L1.y1        -0.303817         0.043074           -7.053           0.000
L1.y2        -0.404604         0.046974           -8.613           0.000
L2.y0        -0.403045         0.048958           -8.233           0.000
L2.y1         0.114061         0.045533            2.505           0.012
L2.y2        -0.606352         0.040094          -15.123           0.000
========================================================================

Results for equation y2
========================================================================
           coefficient       std. error           t-stat            prob
------------------------------------------------------------------------
const         0.288883         0.041383            6.981           0.000
L1.y0        -0.256609         0.046517           -5.516           0.000
L1.y1         0.380568         0.041078            9.265           0.000
L1.y2         0.173060         0.044797            3.863           0.000
L2.y0        -0.074091         0.046688           -1.587           0.113
L2.y1         0.288274         0.043423            6.639           0.000
L2.y2        -0.232047         0.038236           -6.069           0.000
========================================================================

Correlation matrix of residuals
            y0        y1        y2
y0    1.000000 -0.300511  0.090861
y1   -0.300511  1.000000  0.269623
y2    0.090861  0.269623  1.000000
"""

正直、SVARモデルを使う時って、左辺の係数行列$A$が一番注目するところだと思うのですが、その情報が出てこないってところがイマイチですね。このsummary()はVARモデルほどは使わないと思いました。バグが放置されているのものそのせいかな?

最後の結果出力だけイマイチでしたが、必要な値は各属性を直接見れば取れますし、そこそ手軽に使えるモデルではあったのでVARモデルで力不足に感じることがあったらこれも思い出してみてください。

構造VARモデルの紹介

久しぶりに時系列の話です。以前、ベクトル自己回帰モデル(VAR)というのを紹介しました。
参考: ベクトル自己回帰モデル

これは要するに、時系列データのベクトルを、それより前の時点のベクトルで回帰する(線形和として表現する)ことによって説明しようっていうモデルでした。

これを実際に業務で使おうとすると、非常に厄介な問題が発生します。それは、同じ時点での値どうしの間にも関係があるということです。

例えば、何かのECサイトの分析をしてて、サイトの訪問者数、会員登録数、売上、の3つの時系列データがあったとした場合、VARの観点で言うと、過去の訪問者数、過去の会員登録数、過去の売上、からその日の各値を予測しようって言うのがVARモデルです。

そうなった時に、いやいや、会員登録数は「当日の訪問者数」の影響を受けるし、売上は「当日の会員登録数」の影響を受けるでしょうとなります。VARモデルではそう言う影響が考慮できません。ここを改善したのが構造VARモデル(Structural Vector Autoregressive Model)です。

時系列分析でいつも引き合いに出している、沖本先生の「経済・ファイナンスデータの計量時系列分析」では、4.6節(99〜101ページ)でさらっと紹介されています。2ページくらいです。

具体的に、3変数で最大ラグが2のSVARモデルを書き出すと下のような形になります。

$$\left\{\begin{align}y_{0,t}&=c_0&&& +\phi_{0,0,1}y_{0,t-1}+\phi_{0,1,1}y_{1,t-1}+\phi_{0,2,1}y_{2,t-1}+\phi_{0,0,2}y_{0,t-2}+\phi_{0,1,2}y_{1,t-2}+\phi_{0,2,2}y_{2,t-2}+\varepsilon_0\\
y_{1,t}&=c_1&-\phi_{1,0,0}y_{0,t}&&+\phi_{1,0,1}y_{0,t-1}+\phi_{1,1,1}y_{1,t-1}+\phi_{1,2,1}y_{2,t-1}+\phi_{1,0,2}y_{0,t-2}+\phi_{1,1,2}y_{1,t-2}+\phi_{1,2,2}y_{2,t-2}+\varepsilon_1\\
y_{2,t}&=c_2&-\phi_{2,0,0}y_{0,t}&-\phi_{2,1,0}y_{1,t}&+\phi_{2,0,1}y_{0,t-1}+\phi_{2,1,1}y_{1,t-1}+\phi_{2,2,1}y_{2,t-1}+\phi_{2,0,2}y_{0,t-2}+\phi_{2,1,2}y_{1,t-2}+\phi_{2,2,2}y_{2,t-2}+\varepsilon_2\\\end{align}\right.$$

後ろの方、ブログ幅からはみ出しましたね。見ての通りそこそこ巨大なモデルになります。
この、$y_{1,t}$の予測に$y_{0,t}$が使われていたり、$y_{2,t}$の予測に$y_{0,t},y_{1,t}$が使われているのが、VARモデルとの違いです。

逆に、$y_{0,t}$の説明には$y_{1,y}$や$y_{2,t}$は用いられてはいません。

これは変数が外生性が高い順に並んでいることを仮定しているためです。これを仮定せず、相互に影響し合うようなモデルも研究されてはいるようなのですが、実データから係数を推定するのが非常に難しくなるので、SVARモデルを使うときはこの仮定を置いておいた方が良い、と言うよりこれが仮定できる時に利用を検討した方が良いでしょう。

通常は、時刻$t$の時点の項を左辺に移行して行列表記するようです。(そのため、上の例でも移項を想定してマイナスつけときました。)

移行して、行列、ベクトルを記号に置き換えていくと、下のような式になります。$\mathbf{D}$は$n\times n$の対角行列とします。要するに撹乱項はそれぞれ相関を持たないとします。

$$
\mathbf{B}_0\mathbf{y}_t=\mathbf{c}+\mathbf{B}_1\mathbf{y}_{t-1}+\cdots+\mathbf{B}_p\mathbf{y}_{t-p}+\boldsymbol{\varepsilon}_t,\ \ \
\boldsymbol{\varepsilon}_t\sim W.N.(\mathbf{D})
$$

この形の式を構造形(Structual form)と呼びます。VARモデルとの違いは、左辺に$\mathbf{B}_0$が掛かってることですね。

この構造形ですが、実際にデータがあった時にこのまま係数を推定することが難しいと言う問題があります。そのため、両辺に$\mathbf{B}_0$をかけて次の形の式を考えます。

$$
\mathbf{y}_t=\mathbf{B}_0^{-1}\mathbf{c}+\mathbf{B}_0^{-1}\mathbf{B}_1\mathbf{y}_{t-1}+\cdots+\mathbf{B}_0^{-1}\mathbf{B}_p\mathbf{y}_{t-p}+\mathbf{B}_0^{-1}\boldsymbol{\varepsilon}_t
$$

この形を、誘導形(reduced form)と呼びます。撹乱項にも逆行列がかかってるので、誘導形の撹乱項の各成分には相関が生まれている点に気をつけてください。

この誘導形はVARモデルと全く同じなので、VARモデルを推定するのと同じ方法でパラメーターを推定することができます。

そして、ここからが問題なのですが、誘導形から構造形を求めることはそう簡単ではありません。(逆に構造形が事前にわかっていた場合に、誘導形を求めることは簡単です。上でやった通り、両辺に逆行列かけるだけだからです。)

その難しさの原因を説明をします。上の方で、変数が外生性が高い順に並んでると仮定して$\mathbf{B}$の一部の成分が0であることを仮定していましたが、もしその仮定がなかったとしましょう。すると、構造形の方が誘導形よりパラメーターが多くなってしまうのです。

構造形の方は、$\mathbf{y}_0$の係数の行列が、対角成分は1とわかってるので残り$n(n-1)$個、右辺は定数項が$n$個、右辺の各行列が全部で$pn^2$個、撹乱項が$n$個ので、合わせて、$n(n-1)+n+pn^2+n=n(n+1)+pn^2$個のパラメータを持ちます。

一方で誘導形の方は、左辺のパラメーターこそ消えますが、右辺は定数項が$n$個、右辺の各行列が全部で$pn^2$個、撹乱項が$n(n+1)/2$個で、合計$n+pn^2+n(n+1)/2$個しかパラメーターを持ちません。

その差、$n(n-1)/2$個だけ、構造形がパラメーターが多いので、誘導形が定まった時に構造形が決められなくなってしまいます。

そこで、誘導形から構造形を一意に定めるために、$n(n-1)/2$個の制約を課す必要が発生します。その制約として、$\mathbf{B}_0$の上三角成分(対角成分より上)を0と決めてしまうのが、変数が外生性が高い順に並んでいると言う仮定です。

個人的には、この仮定があったとしても誘導形から構造形を導くのってそこそこ難しいように感じますが、それでも理論上は算出が可能になりますね。

具体的にデータを用意してPythonを使ってSVARのパラメーター推定をやってと記事を続ける予定だったのですがここまでで結構長くなってしまったのと、数式のせいでそこそこ疲れてしまったので、それは次回の記事に回そうと思います。

VARの欠点をクリアしたモデルではありますが見ての通り巨大なので、かなりデータが多くないと推定がうまくいかなかったりして、なかなか期待ほど活躍しないのですが、こう言うモデルもあるってことを認識しておくとどこかで役に立つのかなと思います。

SciPyで数列の極大値や極小値を求める

時系列データを分析している中で、極大値や極小値を特定したいケースは稀にあります。

極大値/極小値というのは、要は局所的な最大値/最小値のことで、その値の周囲(前後)の値と比較して最大だったり最小だったりする要素のことです。(とても雑な説明。もっと正確な説明はWikipediaの極値のページを参照。)

例えば、[5, 4, 3, 2, 3, 4, 3, 2, 1] みたいな数列があった時、一番値が大きいのは先頭の5なので、これが最大値(極大値でもある)ですが、6番目の4もその近くだけ見ると、[2, 3, 4, 3, 2]となっていて前後の値より大きいので、この6番目の4が極大値です。

今の説明の通りのコードを書いて数列の前後の値と比較して判定したら極大値も極小値も見つかるのですが、それを一発でやってくれるメソッドがSciPyにあるよ、ってのが今回の記事です。

使うのはargrelmin/ argrelmax です。ドキュメントは以下。
scipy.signal.argrelmin — SciPy v1.10.0 Manual
scipy.signal.argrelmax — SciPy v1.10.0 Manual

minとmaxの違いは極小値か極大値かの違いなので、以下の説明はargrelmaxの方でやっていきますね。

ちょっと適当な数列を一個用意して実行してみます。(さっきの例に値を付け足して長くしたものです。)

import numpy as np
from scipy.signal import argrelmax

# サンプルデータを用意
data = np.array([5, 4, 3, 2, 3, 4, 3, 2, 1, 8, 1])

# 極大値のindex
print(argrelmax(data))
# (array([5, 9]),)

# 極大値の値
print(data[argrelmax(data)])
# [4 8]

indexが5(インデックスは0始まりなので6番目の要素)である4と、indexが9(同様に10番目の要素)である8が検出されました。

printした結果を見ていただくと分かる通り、argrelmaxはindexが入ったarrayを0番目の要素に持つタプル、という特殊な形で結果を返してくれます。慣れないとトリッキーに見えますが、それをそのまま使うと極値の値を取り出せるので便利です。

デフォルトでは、直前直後の値だけを見て極大値極小値が判定されますが、例えばノイズを含むデータなどでは実用上検出が多すぎることもあります。
その場合、order(デフォルト1、1以上の整数のみ指定可能)という引数を使うことで、前後何個の要素と比較するかを指定できます。

order=3 とすると、前の3個の値と、後ろの3個の値、合計6個より大きい場合に極大値として判定されます。

data = np.array([2, 1, 1, 4, 1, 1, 5, 1])

# index3の4 と index6の5が極大値として検出される。
print(argrelmax(data, order=1))
# (array([3, 6]),)

# order=3とすると、index6の5だけが極大値として検出される。
print(argrelmax(data, order=3))
# (array([6]),)

上記の例でもう一個着目して欲しい点があります。order=1の時、先頭の2は極大値として検出されませんでした。デフォルトでは、orderの値に関係なく前後に最低1個の要素がないと対象にならないようです。そして、order=3の場合も、後ろから2番目の5が検出されています。orderで指定した数に足りなくても前後に1個以上あれば良いようです。

この、端に関する挙動はmodeという引数で指定できます。デフォルトは”clip”で、これは両端の値は極値として扱われません。ここに”wrap”を指定すると、両端の値も対象になります。

# index=0の2も対象になった
print(argrelmax(data, order=1, mode="wrap"))
# (array([0, 3, 6]),)

もう一つ気をつけないといけないのは、ドキュメントに書かれている通り、前後の値より真に大きくないと極値として扱われません。以下のように前後の値と一致したらダメということです。

data = np.array([1, 1, 2, 3, 3, 2, 1])
print(argrelmax(data))
# (array([], dtype=int64),)

以上が1次元のデータに対する使い方になります。

さて、このargrelmin/ argrelmaxですが、2次元以上のデータに対しても使えます。ドキュメントには2次元の例が載っていますが、3次元でも4次元でもいけます。

2次元、要するに行列形式のデータに対して使ったら、上下左右、できれば斜めも考慮した8方向の隣接データと比較して極大値/極小値を出してくれるのかな?と期待したのですがそういう動きはしておらず、軸(axis)を1個固定してその軸に沿った1次元データとして取り出してそれぞれに対して極大値/極小値の検索をやるようです。方向はaxis引数(デフォルト0)で指定します。

ちょっとでたらめに作ったデータでやってみます。

data = np.array([
        [1, 4, 0, 9, 0, 3, 2],
        [4, 0, 0, 1, 2, 3, 7],
        [2, 9, 2, 0, 9, 0, 7],
        [0, 0, 7, 9, 6, 3, 1],
        [0, 4, 4, 7, 2, 8, 3]
    ])

# axis省略は0と同じ。
print(argrelmax(data))
# (array([1, 2, 2, 3, 3]), array([0, 1, 4, 2, 3]))

print(argrelmax(data, axis=1))
# (array([0, 0, 0, 2, 2, 3, 4, 4]), array([1, 3, 5, 1, 4, 3, 3, 5]))

結果の読み方が慣れないと分かりにくいですが、インデックスの1次元目の配列、2次元目の配列のタプルとして帰ってきてます。

要するに、 (array([1, 2, 2, 3, 3]), array([0, 1, 4, 2, 3])) というのは [1, 0], [2, 1], [2, 4], [3, 2], [3, 3]が極大値だった、という意味です。

そして、またこれも分かりにくいのですが、axis=0 の時、この行列の各列を取り出して極大値を探しています。[1, 0]の値は4 ですが、これはdata[:, 0] = [1, 4, 2, 0, 0] の極大値として検出されており、[3, 2]の7 は data[:, 2] = [0, 0, 2, 7, 4] の極大値として検出されています。

スライスした時に:(コロン)になる次元をaxis引数で指定していると考えたら良いでしょうか。

引数を省略してた時の挙動が想定と違うというか、axis=1を指定した時の方がデフォルトっぽい動きしていますね。こちらは、
(array([0, 0, 0, 2, 2, 3, 4, 4]), array([1, 3, 5, 1, 4, 3, 3, 5]))
が結果として帰りますが、こちらも同様に[0, 1], [0, 3], [0, 5], ….(略) が極大値として検出されています。そしてこれは data[0, :] = [1, 4, 0, 9, 0, 3, 2] の極大値です。

滅多に使わない関数ですしさらにこれを多次元データに使うというのも稀だと思うので、完璧に理解し記憶して使いこなすというよりも、必要になった時に挙動をテストしながら使うのが現実的ではないでしょうか。

M2搭載のMacBookにPython環境構築 (2023年02月時点)

最近、私物のMacBookを買い替えました。買ったのは 「MacBook Air M2 2022」です。(以前はPro使ってましたが、開発環境はAWS上にもあるのでローカルはAirで十分かなと思って変えました。)

さて最近のMacBookは、CPUがIntel製ではなく、Apple製のものになっています。この影響で特にM1登場当初は環境構築で苦労された人もたくさんいたようですし、すでに多くの記事が書かれてナレッジがシェアされています。僕自身、それが原因で調子が悪かった先代のMBPをなかなか買い替えなかったというのもあります。

しかし現在ではOSSコミュニティーの皆さんの尽力のおかげで環境はどんどん改善しており、僕自身はそこまで大きな苦労なくPython環境を構築できました。とはいえ、あくまでも私用端末なので、業務で使ってる端末に比べると入れたライブラリ等がかなり少ないのですが。それでも、初期の頃つまづいた報告が多かったnumpyやmatplotlibなどは入れれることを確認しています。

現時点ではこんな感じですよ、ってことで誰かの参考になると思うので手順を書いていきます。

前提:
– シェルはzsh
– pyenv利用
– Anaconda/ Minicondaは使わない
– Python 3.11.1 をインストール

では、やっていきます。

1. Homebrewインストール

pyenvを導入するために、まずHomebrewを入れます。
公式サイトに飛んでインストールコマンドを実行します。一応コマンドはこのブログにも載せますが、昔とコマンドが変わっていますし、今後変わる可能性もありますし実行する時にちゃんと公式サイトを確認したほうがいいですね。

公式サイト: macOS(またはLinux)用パッケージマネージャー — Homebrew

# サイト掲載のインストールコマンド
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

以前だったらこれを実行して、メッセージに従って何度かENTERキー押して進めていけば終わりだったのですが、今は最後に以下のようなメッセージが表示されます。(正直、見落としがちなので公式サイトにも書いておいて欲しかった。)

Warning: /opt/homebrew/bin is not in your PATH.
  Instructions on how to configure your shell for Homebrew
  can be found in the 'Next steps' section below.
==> Installation successful!
# -- 中略 --
==> Next steps:
- Run these three commands in your terminal to add Homebrew to your PATH:
    echo '# Set PATH, MANPATH, etc., for Homebrew.' >> /Users/{ユーザー名}/.zprofile
    echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{ユーザー名}/.zprofile
    eval "$(/opt/homebrew/bin/brew shellenv)"
- Run brew help to get started
- Further documentation:
    https://docs.brew.sh

要するにPathが通ってないから通して、とのことです。

Next stepsにある3行を実行します。ただ、1行目はただのコメント文を残すだけですし、3行目のeval はターミナル/シェルを再実行すれば良いだけなので、必須なのは2行目だけです。
この記事ではマスクしてますが、{ユーザー名}部分、ちゃんと端末のユーザー名がメッセージに表示されてます。気が利きますね。

$ echo '# Set PATH, MANPATH, etc., for Homebrew.' >> /Users/{ユーザー名}/.zprofile
$ echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{ユーザー名}/.zprofile
$ eval "$(/opt/homebrew/bin/brew shellenv)"

ちなみに、これをやると環境変数PATHにhobebrew関係のパスが追加されます。気になる人は実行前後で見比べてみましょう。

2. pyenvインストール

Homebrewが入ったら次はpyenvです。ドキュメントにHomebrewを使って入れる専用のセクションがあるのでそれに従います。

ドキュメント: https://github.com/pyenv/pyenv/blob/master/README.md#homebrew-in-macos

$ brew update
$ brew install pyenv

続いて、シェルの設定です。こっちにあります。zshの方をやります。
参考: https://github.com/pyenv/pyenv/blob/master/README.md#set-up-your-shell-environment-for-pyenv

$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
$ echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
$ echo 'eval "$(pyenv init -)"' >> ~/.zshrc

3. Pythonインストール

pyenvが入ったらPythonを入れます。ちゃちゃっと入れたいところですが、そのまいれたら、
WARNING: The Python lzma extension was not compiled. Missing the lzma lib?
という警告が出ました。xzってのを先に入れておくとこれが出ないのでやっておきましょう。homebrewで入ります。

$ brew install xz

ここまでできたらPythonインストールとバージョン切り替えです。

入れたいバージョンがはっきり決まっているならそれをそのまま入れたら良いのですが、僕は毎回その時点でインストール可能なバージョンの一覧を眺めてから決めています。今回は3.11.1を入れました。

# インストール可能なバージョン一覧表示 
$ pyenv install -l 
# インストール 
$ pyenv install 3.11.1
# バージョン切り替え
$ pyenv global 3.11.1

そして、ライブラリを入れていきます。requirements.txt作って一括で入れてもいいのですが、ちょっと怖かったので1個ずつ恐る恐る入れていきましたが、概ねすんなり入っていくようです。正直、Pythonだけで書かれてるライブラリたちは比較的安心なのですが、numpy等のC言語が使われているライブラリは不安でしたね。

$ pip install jupyterlab
$ pip install numpy
$ pip install pandas
$ pip install scipy
$ pip install matplotlib
$ pip install scikit-learn
$ pip install lxml
$ pip install requests
$ pip install beautifulsoup4
# 以下略

以上のようにして、M2 MacでもPythonが使えるようになりました。

対応を進めていただいた開発者の皆様、ありがとうございました。

LINE NotifyでPythonから自分にメッセージを送る

前回に続いて通知を作る話です。
今回はLINE通知を作ります。

この記事は、企業が運用している本格的な公式アカウントやBotサービスのようなものではなく、個人的に運用しているサーバーのバッチでエラーが起きたときなどに自分宛に通知するという小規模な利用を想定して書きます。

利用するサービスはこちらの LINE Notify です。
参考: https://notify-bot.line.me/ja/

この種のSNSに付随したサービスに対しては認証周りで面倒なコードを書かないといけないイメージがあったのですが、LINE Notifyは非常にコンパクトな実装で手軽に使えました。

事前準備として、こちらのサービスからトークンを入手します。

これ専用のアカウントは必要なく、普段使っているLINEのアカウントでログインできます。右上のログインリンクからログインしましょう。(LINE認証用のコードが表示され、LINEアプリから入力が必要なので、スマホも用意しておきましょう。)

ログインしたら、右上のログインリンクが自分のLINEアカウント名になっているので、それを押してマイページへ遷移します。
そして、「アクセストークンの発行(開発者向け) 」のところから「トークンを発行」ボタンをクリックします。

トークン名を入力し、通知を送るトークルームを選択して、「発酵する」ボタンをクリックするとトークンが発行されて1度だけ表示されます。もう二度と表示されないのでこの時点で確実に記録しておきましょう。

先に書いておきますが、通知を実装した後実際にLINEに届くメッセージは、
[トークン名] メッセージ本文
というフォーマットになります。トークン名が長いと毎回邪魔なのでコンパクトでわかりやすい名前にしておきましょう。

さて、トークンが発行できたらこれを使ってみます。
ドキュメントはこちらです。
参考: LINE Notify API Document
このドキュメントの「通知系」のところにある、https://notify-api.line.me/api/notify が通知を送るAPIです。

リクエストパラメーターがいろいろ書かれていますが、「必須」と指定されているのはmessageだけなので非常に簡単に使えます。

CURLで動かすサンプルコードもあるのでちょっとやってみましょう。{自分のトークン}の部分は先ほど発行したトークンを入れてください。

# コマンド
$ curl -X POST -H 'Authorization: Bearer {自分のトークン}' -F 'message=CURLで通知' https://notify-api.line.me/api/notify

# 結果
{"status":200,"message":"ok"}

これで、「[トークン名] CURLで通知」というメッセージが、 LINE Notify のアカウントから届きます。

あとは、このcurlコマンドをPythonに書き直していきましょう。使うライブラリはrequestsあたりで良いと思います。

import requests


line_notify_token = "{自分のトークン}"
api_url = "https://notify-api.line.me/api/notify"
message = "メッセージ本文"
headers = {"Authorization": f"Bearer {line_notify_token}"}
data = {"message": message}
requests.post(
    api_url,
    headers = headers,
    data = data,
)

たったこれだけで、LINEにメッセージが届きます。

もう少し丁寧に実装するなら、postの戻り値のstatusコードを確認してエラー処理を入れたりするとよさそうですね。

LINEのインターフェース的に、あまりにも長文を送ったりするのには適さず、長文になるなら先日のメール通知の方が良いかなと思うのですが、
メールよりLINEの方が通知に気付きやすいので、速報性が必要な場面で重宝しそうです。

PythonでYahooメールのアカウントからメール送信

個人的に運用しているソフトウェアに通知機能を作りたかったので、メールの送信方法を調べました。本当はSlack通知とかの方が使いやすいのですが、最近のSlackの無料プランは一定期間でメッセージが消えるなどイケてないですからね。

メールの利用を検討し出した当初はAmazon SESを使おうかとも思っていたのですが、標準ライブラリだけで実装できることと、料金もかからないのでPythonでやることにしました。

使用するライブラリは以下の二つです。
– SMTPプロトコルクライアント – smtplib
– メールメッセージの管理 – email
email の方は 使用例のページを見た方がいいです。

使い方はYahooメールやGmail, Outlookなどアカウントによって微妙に違うので今回はYahooメールを例に取り上げて説明します。

メールサーバーやポート番号の情報はこちらにあります。
参考: メールソフトで送受信するには(Yahoo!メールアドレス、@ymail.ne.jpアドレスの場合)

必要な情報は以下の2つです。
– 送信メール(SMTP)サーバー : smtp.mail.yahoo.co.jp
– 送信メール(SMTP)ポート番号 : 465

実はこの情報は、2020年8月に変わっていて、他所の古い技術記事等ではポート番号が違っていたりします。他サイトのコードをコピペしたが動かなかったという人は以下のアナウンスを読んでください。SMTPの通信方法がSSLになっているというのもライブラリで呼び出す関数が変わるので重要な点です。
参考: Yahoo!メールをより安全にご利用いただくためのメールソフト設定(送受信認証方式)変更のお願い

メールソフトで送受信するにはのページに記載がありますが、「Yahoo! JAPAN公式サービス以外からのアクセスも有効にする」の設定をやっておかないと動かないので気をつけてください。(とはいえ、普段スマホでメールを見れるようにしているのであれば設定済みだと思います。)

それではやっていきましょう。自分のスクリプトから自分のメールアドレス宛の通知としての利用を想定しているので飾りも何もないテキストメールを送ります。

まず、各変数に必要な値を格納しておきます。悪用は厳禁ですが、toだけでなくfromのメールアドレスも自由に設定できます。まともに使うならfromは自分のアドレスでしょう。

# Yahooのログイン情報
username = "{YahooのユーザーID}"
password = "{Yahooのパスワード}"

# メールアドレス情報
from_address = "{差出人のアドレス}@yahoo.co.jp"
to_address = "{宛先のアドレス}@yahoo.co.jp"

# SMTPサーバーの情報。値はYahooメールのヘルプページから取得。
smtp_host = "smtp.mail.yahoo.co.jp"
smtp_port = 465

続いて、メールの情報を作ります。smtpのドキュメント末尾では文字列で直接データを作ってますが、その直下の注釈でemailパッケージを推奨されているのでそちらに従います。

from email.message import EmailMessage


msg = EmailMessage()
msg.set_content("メール本文")
msg["Subject"] = "メールタイトル"
msg["From"] = from_address
msg["To"] = to_address

メールデータできたので、これを送信します。ドキュメントの一番下のサンプルコードではローカルのSMTPサーバーでメール送信しているのでログインも何もしていませんが、Yahooメールを使うなら最初にログインが必要です。smtplib.SMTP ではなく、smtplib.SMTP_SSLを使うのもポイントですね。コードは以下のようになります。

import smtplib


server = smtplib.SMTP_SSL(smtp_host, smtp_port)
server.login(username, password)
server.send_message(msg)
server.quit()

たったこれだけでメール送信が実装できました。

WordPressのログインページのURLを変更する

前回の記事で書いてる通り、機械的にログインを試みる攻撃を受けていたので対策を施しました。特定IPアドレスからの攻撃だったのでそのIPをブロックしようかと思ったのですが、他のIPに変えられるたびにやるのも面倒なので、Wordpressのセキュリティ策としてよく挙げられているログインURLの変更を実施しました。

初期設定のログインURLはWordpressのドキュメントを見れば分かっちゃいますからね。

方法はいろいろありますが、手軽な方法としてプラグインを使うことにしました。

選んだのは、 All In One WP Security です。もっとシンプルな、URLの変更に特化したやつとかもあるのですが今後別の対策を考える時があったら使いまわせるのがいいと思ったのでこれを選びました。(結果的にURL変更だけで攻撃が収まったのですが、対策前の時点ではそれだけで完了するかどうわかりませんでしたし。)

WordPressの管理画面からプラグインの画面を開き、新規追加から検索してインストールします。そして有効化します。

有効化したら左ペインのメニューに「WPセキュリテイ」というのができるのでここから設定します。

「総当たり攻撃」というカテゴリ(英語だとBrute force)の中の、Rename login page タブがログインURLの変更です。それを開きます。

Enable rename login page feature: のチェックボックスにチェックを入れ、Login page URL: のテキストボックスの中にこれから使うURLを設定しSaveするとそちらが新しいログインページになります。

これで、デフォルトのログインページにアクセスしてみてフォームが出てこないことを確認したら完成です。

WordPressは未ログイン状態でログイン後の管理画面のURLにアクセスするとログインURLにリダイレクトされたりするのですが、このツールでURLを変更するとその動作もなくなり、リダイレクトによってURLがバレるということも防いでくれています。気がききますね。

そして、肝心の攻撃に対する効果ですが、apacheのログを見ると該当の攻撃者に404エラーが出た段階でピタリとアクセスが止まってるのを確認できました。Lightsailのリソースも回復しており良い感じです。

今回の事象への対応はこれで完了ですが、時々不自然にアクセスが集中している時間があったりなどの不穏な動きはまぁまぁあるので必要に応じて今回導入したAll-In-One Securityの各機能を活用して対策して行こうと思います。

Lightsailで立てたWordPressサーバーのapacheログについて

新年のご挨拶でちょっと書きましたが、このブログが昨年末に攻撃を受けていたようで、過剰なアクセスによりCPUリソースが枯渇する事態となっていました。

下にコンソールで確認したCPUリソースの画像を貼りますが、パーストキャパシティがなくなってますね。この時間帯、ブログの表示が非常に遅くなってしまっていました。最終的にどうやって対策し事象を解消したたかは次の記事に書くとして、この時の状況調査のためにログファイルを確認したのでその時調べたあれこれを記事にまとめておきます。

このCPUが異常に利用されていた時なのですが、ブラウザではなくコマンドかプログラムか何かしら機械的なアクセスがされていたようで、Google Analyticsでは特にアクセスの増加等が見られませんでした。GAはブラウザでアクセスしてJavaScriptが動かないとデータが取れませんからね。

ということで、サーバー側のログを調べる必要性が発生したわけです。

通常の構成であれば、apacheのログはデフォルトでは、/var/log/ の配下にあるそうです。
/var/log/apatche か、 /var/log/httpd/ のどちらかの下に。

ただし、LightsailのWordpressはbitnamiというパッケージが使われており、apache自体が通常と違う場所にあって、ログファイルも普通と違う場所にあります。ちなみにapacheがインストールされている場所は次のようにして確認できます。

$ which httpd
/opt/bitnami/apache2/bin/httpd

/opt/bitnami の配下にあることがわかりますね。

そして、ログファイルもこの近辺にあります。/opt/bitnami/apache2/log ってディレクトリがあるのです。一応中見ておきますか。

$ ls /opt/bitnami/apache2
bin  bnconfig  build  cgi-bin  conf  error  htdocs  icons  include  logs  modules  scripts  var


$ ls /opt/bitnami/apache2/logs/
access_log              access_log-20210801.gz  error_log-20200223.gz  error_log-20210808.gz
access_log-20200223.gz  access_log-20210808.gz  error_log-20200302.gz  error_log-20210816.g
access_log-20200302.gz  access_log-20210816.gz  error_log-20200308.gz  error_log-20210822.gz
access_log-20200308.gz  access_log-20210822.gz  error_log-20200316.gz  error_log-20210829.gz
#########
#  中略  #
#########
access_log-20210704.gz  access_log-20221218.gz  error_log-20210712.gz  error_log-20221226.gz
access_log-20210712.gz  access_log-20221226.gz  error_log-20210718.gz  error_log-20230101.gz
access_log-20210718.gz  access_log-20230101.gz  error_log-20210726.gz  httpd.pid
access_log-20210726.gz  error_log               error_log-20210801.gz  pagespeed_log

名前から明らかですが、access_logがアクセスログで、error_logがエラーログであり、日付がついて拡張子が.gzになっているのがログローテションで圧縮された古いログです。

ちなみにこのログファイルのパスは、次の設定ファイルで設定されています。

$ vim /opt/bitnami/apache2/conf/httpd.conf

# 中略

<IfModule log_config_module>
    #
    # The following directives define some format nicknames for use with
    # a CustomLog directive (see below).
    #
    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
    LogFormat "%h %l %u %t \"%r\" %>s %b" common

    <IfModule logio_module>
      # You need to enable mod_logio.c to use %I and %O
      LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
    </IfModule>

    #
    # The location and format of the access logfile (Common Logfile Format).
    # If you do not define any access logfiles within a <VirtualHost>
    # container, they will be logged here.  Contrariwise, if you *do*
    # define per-<VirtualHost> access logfiles, transactions will be
    # logged therein and *not* in this file.
    #
    CustomLog "logs/access_log" common

    #
    # If you prefer a logfile with access, agent, and referer information
    # (Combined Logfile Format) you can use the following directive.
    #
    #CustomLog "logs/access_log" combined
</IfModule>

# 中略
#
# ErrorLog: The location of the error log file.
# If you do not specify an ErrorLog directive within a <VirtualHost>
# container, error messages relating to that virtual host will be
# logged here.  If you *do* define an error logfile for a <VirtualHost>
# container, that host's errors will be logged there and not here.
#
ErrorLog "logs/error_log"

#
# LogLevel: Control the number of messages logged to the error_log.
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
#
LogLevel warn

CustomLog / ErrorLog がファイルパスの指定で、LogFormatとしてログの出力書式も指定されていますね。
書式の%hとかの意味はこちらのドキュメントにあります。
参考: mod_log_config – Apache HTTP サーバ バージョン 2.4

あとは中身を確認したら良いです。access_log とかのファイルはapacheがアクセスがあるたびにバリバリ書き込んでる物なので、ロックがかからないようにこれを直接開くのは避けて、どこかにコピーして開きましょう。scp等でローカルに持ってきちゃうのが良いと思います。

拡張子が.gzのものは、gzip コマンドに -d オプションをつけて実行すると解答できます。

# *(アスタリスク)を使ってまとめて解凍しちゃうと楽。
$ gzip -d *.gz
# .gzファイルが無くなり、解凍済みファイルだけが残ります。

あとはただのテキストファイルなので、出来上がったファイルを確認したら良いです。

この結果、冒頭に挙げた攻撃を受けてた時間帯は、ログインを試みるアクセスが特定のIPアドレスから7万回も発生していたのがわかりました。

基本的に通常のアクセス分析はGoogle Analyticsを見れば済む話なのでapacheのログに意識をはらってきませんでしたが、今回調査してみてもっと使いやすいフォーマットで出力するように設定しておけば良かったなと思いました。csvではないのでpandasでのパースも面倒でしたし、User Agentなど取れるはずなのに取ってない情報も多かったので。そしてタイムゾーンも日本時間じゃないんですよね。これも地味に扱いにくいです。

2023年のご挨拶

新年明けましておめでとうございます。本年もこのブログをよろしくお願いします。

早速ですが今年のこのブログの更新方針を決めました。昨年同様に今年もしっかりインプットの時間を確保し、ブログへのアウトプットは少なめに週1回の更新を目指していきたいと思っています。

今年は新年早々から統計数理研究所の講座受講も2件決まっていたり、参加したいセミナーやミートアップも既にいくつかあるので積極的に動いていきたいです。昨年からすうがくぶんか社のセミナーも受講していますが、今年も何か面白そうなのを探して受講しようと思います。また、書籍についても昨年後半出た本が複数あり、まだ追いついていないので順次読んでいきます。

昨年はインプットを増やすと言って一番増えたのがビジネス系Youtubeの視聴時間だったので、今年はちゃんと読書時間を増やしたいです。Youtubeは最初は良かったのですが、冷静に見ると似たようなネタの繰り返しが多くてそろそろ減らしていいかなと思ってます。

このブログはネタ帳を用意していてそこに常時数十個のテーマを列挙しており、そこからその日の気分でピックアップして書いています。大体その執筆時点で新しく知ったばかりのことを優先的に選んで書いてるのですが、そうやって場当たり的に書いていると、タイミングを逸していつか書きたいと思ったまま放置状態になってしまっているテーマがたくさん残ってしまいました。この点は反省していて、そのうち書こうと思っていたけど放置してた系の記事をもっと書くようにしたいなと思っています。

例えば以下のような内容がいつか書かねばと思って放置された状態です。物によってはブログ開設前(2018年)にリストアップしてその時からずっと放置しています。全部書けるかというと難しそうなのですが少なくとも半分程度はクリアしたい。
– グラフのコミュニティー検出
– AWSの各サービスについて(DynamoDB/ personalize/ Forecast/ SageMaker など)
– opencv
– 生存分析(カプラン・マイヤー法やCOX回帰など)
– node2vec
– scikit-learn等のライブラリの最近の新機能
– 因果探索(LiNGAMなど)/因果推論
– 時系列データの異常検知や変化検知
– 状態空間モデル(カルマンフィルター)
– JavaScriptのデータ可視化関数(特にワードクラウド)
– Word Mover’s Distance などの自然言語処理の小ネタ
– jupyter lab
– J-Quants API
その他、numpy, scipy, pandas, matplotlib, tableauなどの小ネタなどが多数。

今年は今年で新ネタは出ると思いますし、更新回数が50回程度と考えるともう1年分のネタは確実に確保できそうです。あとは実際に執筆する時間とモチベーションを維持できるかという点が問題ですね。(何せ、書ける状態なのに書かなかったネタたちなので1つ1つがちょっと重い。)
できる範囲で頑張って書いていこうと思いますのでよろしくお願いします。

この他、昨年目標に入れていてあまり手をつけなかったこのブログ自体のメンテナンスもやらなければなりません。PHPやバージョンアップとか。これLightsail使ってるとすごく面倒なんですよね。
また、この記事を書いてる時点で海外から攻撃を受けているようでして、どこかの誰かが執拗にLoginを試みていてそのアクセスでCPUリソースが枯渇しているようです。
ここがそんなハッキングする価値のあるブログだとは思えなのですが、攻撃してくる人がいる以上はセキュリティ面の強化等も進めなければなりませんので、何かやったら記事にしていこうと思います。
アクセスが重くなっていることがあり、訪問者の方にはご不便をおかけします。

訪問者の方にはあまり関係ないことなのですが、Google Analyticsの旧バージョン、ユニバーサル アナリティクスが今年終了するというのもブログ関係では大きなイベントですね。
後継のGA4をしっかり学んで、継続して分析ができるようにしたいと思います。
(ただ、現時点のGA4は明らかにUAに劣るように感じているので、他の分析ツールへの乗り換えも視野に入れたい。これから改善するといいのですが。)

ブログ以外では、昨年からやっている投資ツール開発の個人プロジェクトももっと進めていきます。プログラムはほぼ動くものが揃ってきているのであとは手動で実行から自動実行への切り替えとか自動実行に伴うエラー通知の仕組み構築とかが残課題です。

以上のような方針で今年も頑張っていこうと思いますので、本年もよろしくお願いいたします。

2022年のまとめ

今日は2022年最後の月曜なので、この記事が2022年最後の記事です。1年間毎週の更新を継続できてほっとしています。

今年は年初に書いた方針通り、昨年に比べて更新頻度を半分に落としました。しかしそれでも昨年以上に多くの方に訪問していただけました。昨年も書いていますが、休日も夜間も継続的にアクセスがあり、いつも自分以外にも、どこかで技術的な調査や勉強に取り組んでいる人がいると実感できることは自分自身にとっても励みになりました。また、TwitterなどのSNSや他のブログ等で引用されていることを見かけることも多く、自分が書いた記事が誰かの役に立っていると実感でき、そのおかげでアウトプットを続けてくることができました。

まとめの記事なので、今年も1年間の振り返りをやります。本年までの累積の記事数および、年間のアクセス数は次のようになりました。

– 累計記事数 566記事 (この記事含む。昨年時点 514記事)
– 訪問ユーザー数 272,075人 (昨年実績 200,661人)
– ページビュー 476,587回 (昨年実績 348,595回)

更新数落として昨年比で+33%の訪問者数というのは本当に嬉しいです。最近では平日は1日1800人ものかたに訪問していただいています。

現行のGoogleアナリティクス(UA)が来年6月まででサービス終了してしまうので、来年はこの集計をどうするか考えないといけないですね。

今年もよく読まれた記事ランキングを見ていきましょう。以前は半年おきにやっていたのですが、今年は更新数を減らしたのもあって上期にやらなかったので1年ぶりです。
2022年1年間でのPV数によるランキングは次のようになりました。

1. Pythonで日付の加算、特にnヶ月後やn年後の日付を求める方法 (New)
2. matplotlibのグラフを高解像度で保存する (昨年1位)
3. matplotlibでグラフ枠から見た指定の位置にテキストを挿入する (昨年8位)
4. Pythonのリストをn個ずつに分割する (New)
5. matplotlibのdpiとfigsizeの正確な意味を調べてみた (New)
6. Pythonで連続した日付のリストを作る (昨年3位)
7. globでサブフォルダを含めて再帰的にファイルを探索する (New)
8. PythonでBase64エンコードとデコード (New)
9. Pandasで欠損のある列の文字列型の数値を数値型に変換する (New)
10. PythonでMeCabを動かそうとしたらmecabrc ファイルが無いというエラーが出たので原因を調べた (New)

今年新規にランクインした記事が7記事となりました。matplotlibのグラフの解像度を設定する話は長いことこのブログの一番人気だったのですがついに入れ替わりましたね。(データサイエンス要素は薄いのでこれが人気というのは若干複雑な気持ちです。)
ただこの中で、今年書いた記事って10位のmecabrcの記事だけのような。まぁ、古い記事が強いというのは長期にわたってニーズがある記事を書けているということでもあるので、今年書いた記事たちも来年以降に期待しましょう。

1年間の終わりなので、年初に立てた方針の振り返りもやっておきます。
参考: 2022年のご挨拶と今年の方針

まず、アウトプットは減らしてDSに限らず幅広い範囲のインプットを重視したいという話については、ある程度達成できたが、思っていたのとは違う形になったというのが正直なところです。データ分析の分野では、有償の講座受講などを増やし今までと違った形での学習機会を得ることができました。また、データサイエンス系の書籍の読書量は減らしたとはいえゼロにはしておらず、一定量の継続もできています。

また、仕事に関係ないところでもいつか読みたいと思っていた漫画のシリーズをいくつも読破できましたし、都内各地のいつか行ってみたいと思っていたところへ観光に行くこともできました。特に、上野の国立科学博物館は行ってよかったですね。次は特別展も見てみたいです。

若干想定外だったのは、今年1年間、Youtubeの視聴時間が急激に伸びたことです。人材業界で働いているので転職や就職などのキャリア関係のチャンネルをよく見ました。他にもエンジニア教育、数学を中心とした科学など幅広く見ています。近年Youtuberが増えて配信してる人は収益化が大変だという話を耳にしますが、視聴者としては良質なコンテンツも増えており大変勉強になります。書籍に比べてダラダラ見ることもできるのもいいですね。これは年初は全く想定してなかった変化でしたが良い結果になったかなと思います。

一方で、Youtubeの視聴時間の増加の割をくった形になったのが、データサイエンス以外の分野のビジネス書を読む時間で、これは計画の半分くらいしか進まなかったなと思います。来年改めて取り組みたいです。

このブログ自体のメンテナスをやるぞ、という目標もあったのですがこれが全然進みませんでした。リンクやカテゴリの見直しなどはまだいいとして、PHPのバージョンが古いとか流石に放置しておくのは良くない問題も出ているのでこれは来年対応したいです。

目標には入っていませんでしたが、今年やった取り組みとしてGithubにプライベートリポジトリを立てて、自分一人のプロジェクトを始めたというのもあります。実は17年ほど投資をやっていてExcel VBAで自作したツール群を使っていたのですが、これらをAWSとPythonで書き直していきました。いつかAWSに移行したいと7年くらい前から思ってたのになかなか着手できなかったプロジェクトを進めることができたのは自分にとっては大きかったです。このプロジェクトはこれからも続けていきたいですね。

来年のこのブログをどうするかは、仕事以外も含めて一通り目標を立ててその中でしっかり決めていきたいと思います。来年は2日かその翌週9日かが最初の記事になると思いますが、それまでに方針固めます。

それではみなさま、今年も1年間ありがとうございました。また来年もよろしくお願いいたします。