Looker Studio やさしいはじめの一歩〜実際に触って理解するレポート作成ワークショップ〜

この記事は個人ブログと同じ内容です

www.ritolab.com


蓄積した大量のデータを収集・分析・加工し、事業戦略の意思決定を支援する。いわゆる「ビジネス・インテリジェンス(BI)」という言葉が当たり前に聞かれるようになって数十年。これまでたくさんのツールが生まれてきました。

今回は、データを視覚化するためのツール Looker Studio の使い方を解説します。

Looker Studio

Looker Studioは、Google が提供する BI ツールです。さまざまなデータソースからデータを収集し、視覚化されたレポートやダッシュボードを作成・共有することができます。

Looker Studio

Looker Studio は、Google アカウントを持っていれば誰でも無料で使うことができます。(厳密には有料の機能があります。本記事では無料の範囲で実施します。)

対象

本記事は、Looker Studio を用いてデータの可視化を行いたい人が対象です。

  • ワークショップ(ハンズオン)形式で構成しており、実際に手を動かしながら、記事の通りにレポート作成を行っていくことで、Looker Studio に関する基礎的な理解を深められます。
  • エンジニアでなくても実施できるように構成しています。

レポートの作成を開始しよう

何はともあれ、使ってみましょう。Looker Studio にアクセスし、「使ってみる」ボタンを押下します。

レポート一覧の画面に遷移します。

まだ何もレポートを作成していないため、空っぽの状態です。

「空のレポート」を押下します。

アカウント設定のモーダルが表示されるので、入力と同意を行い、「続行」ボタンを押下します。

アカウント設定が完了したら、再度「空のレポート」を押下しましょう。

レポートの作成画面に遷移しますが、「データのレポートへの追加」が開くので右上のバツを押下して閉じておきます。

これでレポートの作成を開始できました。

ここにグラフなどを設置してレポートを仕上げていくことになります。レポートの名前は、画面左上の「無題のレポート」と表示されているところを押下すれば変更できます。

データを追加しよう

チャート(グラフ)を描画するためには、元となるデータが必要です。

データを読み込んでみましょう。

今回は、練習用のデータとして「とある店舗でのアイスクリームの販売履歴」をスプレッドシートで用意しました。

データ概要

  • データ日時範囲:
    • 2023/01/01 〜 2024/12/31 までの 2 年間
  • データ件数:
    • 10000 件
  • 店舗:
    • 店舗 A, B, C, D の 4 店舗
  • アイスクリーム品目:
  • リピーターによる購入かどうか:
    • 1: YES
    • 0: NO

練習用データは公開しているので誰でもアクセスできます。適宜、ご自身のスプレッドシートにコピーするなどして利用してください。

LookerStudioはじめの一歩サンプルデータ(スプレッドシート)

1. 「データを追加」ボタンを押下します

2. 「Google スプレッドシート」を押下します。

このとき、「Looker Studioに Google スプレッドシートへのアクセス権を許可してください。」という表示が出た場合は「承認」ボタンを押下し、自身の Google アカウントにログインします。

3. Google スプレッドシートの選択画面で、追加したいデータのシートを選択します。

  1. スプレッドシートを選択
  2. シートを選択
  3. 「追加」ボタンを押下

このとき、「このレポートにデータを追加しようとしています 」というモーダルが表示されたら「レポートに追加」ボタンを押下します。

しばらく待つと、データのレポートへの追加選択画面が非表示になり、元の画面に戻ってきます。

データの追加に成功しました。

データを読み込んだ際に作成された表は利用しないため削除してしまいましょう。

これでデータの読み込みは完了です。

チャートを作成してみよう

それでは実際にチャートを作ってみましょう。手始めに「売上の推移」をチャートにしてみます。

1. チャートの設置

「グラフを追加」ボタンを押下します。

「縦棒グラフ」のボタンを押下します。

「クリックして追加するか、ドラッグして描画します」という枠が現れるので、チャートを作成したい場所を選んでクリックします。

棒グラフが追加されました。

グラフの右下をドラッグして引っ張ってあげれば、グラフ描画の領域を広げることができます。

見やすくなるようにグラフの描画領域を広げておきましょう。

2. チャートの設定

設置したチャートを操作して、売上の推移の棒グラフを作成していきます。

棒グラフを選択した状態で、右側にあるメニューを操作していきます。

01. ディメンションの指定

「ディメンション」を見ると「店舗」になっています。ここを「購入日時」に変更します。

グラフが変化したのに気づきましたか?ディメンションを「店舗」から「購入日時」に変更したことによって、これまで X 軸は「店舗」でしたが、「購入日時」になりました。

02. 指標の指定

次に、「指標」を指定します。指標には「売上」を指定します。先程と同じ要領で、「Record Count」から「売上」に変更しましょう。

グラフがまた変化しましたね。Y 軸が売上になりました。

03. チャートの並べ替え

さて、ここで棒グラフを見てみると、並び順がなんだかよくわからないことになっていることに気づきます。日時がバラバラに並んでしまっているようです。

これは、グラフの並び順が「売上の降順(大きい順)」に設定されていることが原因です。

現在作成しているチャートは「売上の推移」ですから、日時がきちんと順番に並んでほしいですね。

並べ替えを「購入日時の昇順」に変更しましょう。そうすることで、グラフの並びが購入日時の古い順になります。

ディメンションと指標

ディメンションとは、を指しています。

グラフを作成する際に、「何を軸として見るか」をここに指定します。今回は「購入日時」を指定しました。時系列を軸として見たいからです。

指標とはその名の通り、評価する値を指します。

グラフを作成する際に、「どの数字を評価したいか」をここに指定します。今回は「売上」を指定しました。

つまり、このグラフでは「購入日時を軸として、売上を見る」=「購入日時の経過に対する売上の推移」というグラフを作成したことになります。

このように、順序立てて考えると、ディメンションには何を指定するべきか、指標には何を指定するべきか。が簡単にイメージできます。

ディメンション、指標にはそれぞれ何を指定すれば良いのか迷った時は、「何を軸にして、何の数値を評価したいのか」を考えてみてください。

チャートを便利にしてみよう

1. 日次・月次・年次で集計したグラフにする

さて、ここまでで売上推移のグラフが作成できましたが、よく見てみると、日時(年/月/日 時:分:秒)ごとでグラフが並んでいます。

これは、「この日時に売上があったものを集計してグラフに表示してる」という状態になっています。

いくらなんでも、時・分・秒という細かい粒度で売上を集計して見ても何かの気づきは得られにくいですよね。せめて、日・週・月・四半期・年などで集計してグラフに表示したいところです。

この操作を行っていきましょう。

01. 購入日時を日・月・年に丸める

現在、ディメンションには「購入日時」を指定していますが、ここを操作し、購入日時を日・月・年に丸めていきます。

ディメンション「購入日時」の左にカレンダーのアイコンがあるので、そこを押下します。

すると、小さなポップアップが開くので、「データの種類」を 「年」に変更します。

ディメンションが「購入日時(年)」となり、棒グラフが年ごとの売上に集計されて表示されました。

続いて、同じ要領でディメンションを「月」に変更してみましょう。データの種類は「年、月」を選択します。

月次で集計された棒グラフになりました。

最後に日次の棒グラフを表示させましょう。

ここまでくればもうおわかりですね。ディメンションを「日付」にすれば、日次で集計されたグラフにすることができます。

日次で集計された棒グラフになりました。

2. 日次・月次・年次を切り替えられるようにする

チャートから気づきを得るためには、日次だけ、月次だけ、年次だけといったように集計単位を固定せずに見られることが大切です。

先程までは、これらのいずれかにグラフを設定しましたが、これを切り替えて見られるようにしましょう。

01. ドリルダウンの有効化

ディメンションに「ドリルダウン」のトグルスイッチがあります。

これを有効化します。すると、ディメンションに 3 つの「購入日時(日付)」 がセットされます。

02. ディメンションの指定 

この 3 つのディメンションに対して、上の 2 つをそれぞれ上からから「購入日時(年)」「購入日時(年、月)」に変更します。

03. デフォルトドリルダウンレベルの指定

ディメンション下部に「デフォルトのドリルダウンのレベル」という項目があります。

現在は「購入日時(日付)」となっていますが、ここを「購入日時(年、月)」に変更します。

棒グラフの表示が月次集計の表示に変化しました。この指定によって、このレポートを開いた時は、このグラフは月次で集計されたグラフとして表示されるようになります。

これで集計単位の設定が完了しました。

04. グラフの集計単位を切り替える

実際にグラフの集計単位を切り替えてみましょう。

グラフの右上に、いくつかのアイコンが並んでいます。

このアイコンの中の「上矢印 」と「下矢印 」が集計単位を切り替えるボタンとなります。

ドリルアップは、集計を現在より 1 つ高レベルに変更します。

ドリルダウンは、集計を現在より 1 つ低レベルに変更します。

現在、ドリルダウンのデフォルトは「月次」ですので、ドリルアップを行えば「年次」の表示に、ドリルダウンを行えば「日次」になります。

実際に押下してみてください。棒グラフの集計が年次・月次・日次で切り替わります。

年次表示

月次表示

日次表示

3. 棒グラフに表示する棒の数を調整する

さて、現在のグラフを月次で確認してみましょう。

読み込んだデータは 2023 年 1 月〜 2024 年 12 月まであるはずですが、グラフに表示されているのは 2023 年 10 月までです。

棒グラフにおいて、いくつの棒を表示するかというのはスタイルで設定できます。

現在は「棒の数」が 10 と指定されているため、グラフには 10 本の棒が表示されているという状態になっています。

例えばこれを 100 に設定すれば、最高で 100 本の棒が描画されるようになるため、より多くのデータを確認できることになります。

ただし、何事も多ければ良いというわけではありません。例えば棒の数 100 本の設定で日次集計で表示するとどうなるでしょう。

逆多すぎて見にくいですね。例えば先月分だけ見たい場合があったとすると、この細さは辛いです。

このように、ピンポイントで「この期間だけ見たい」という場合もあるでしょう。

次でその方法を紹介します。

4. 表示するグラフの期間を選択できるようにする

現在、グラフの棒の数は 10 に設定しています。

ここから、期間指定を行い、その期間のグラフを表示させていきます。

01. コントロールの追加

画面上部にある「コントロールの追加」を押下し、「期間設定」を選択します。

「クリックして追加するか、ドラッグして描画します」という枠が出てくるので、グラフを追加した時と同じ要領で、棒グラフの上部の空いているスペースを押下します。

「期間を選択」というプルダウンメニューが設置されます。これが期間指定を行う際のコントローラーになります。

02. 実際に期間指定をしてみる

日付指定のプルダウンメニューが設置されたので、実際に日付範囲を指定してみましょう。

2024 年 1 月〜 2024 年 6 月までを指定してみます。日付を指定したら、「適用」ボタンを押下します。

すると、2024 年 1 月〜 2024 年 6 月までのグラフが描画されました。

これで、見たい日付の範囲でグラフが見られるようになりました。

03. デフォルトの期間指定

データは 2024 年 12 月までありますが、デフォルトで表示されているのは 2023 年 1 月〜 10 月のグラフです。

どうせなら古いデータのグラフではなく、現在の年月データを棒グラフに含めて表示させたいので、これを設定していきます。

現在の年月から 3 ヶ月間のデータを、デフォルトで表示させるようにしましょう。

期間指定のコントローラーを選択状態にすると、右メニューに「デフォルトの日付範囲」という項目が表示されます。

デフォルトの日付範囲は 「自動期間」に指定されていますが、ここを変更していきます。

デフォルトの日付範囲のプルダウンメニューを開くとカレンダーが表示されますので、右上に「自動期間」と表示されているプルダウンメニューをさらに開きます。

プルダウンメニューより「詳細設定」を選択します。

するとカレンダーの表示が変化するので、開始日の欄で上から 3 つ目の数値を 3と入力し、 4 つ目の欄を「」に指定します。

入力したら「適用」ボタンを押下します。

これで「開始月は現在から 3 か月前」という指定を行ったことになります。

記事執筆時点は 2023 年 11 月ですので、レポートを開くと自動で 2023/08 〜 11 月までのグラフが表示されるようになります。

これでデフォルトの期間指定は完了です。

はじめの一歩、おめでとうございます。

Looker Studio で売上推移のチャートを作成しました。成果物として、売上推移のグラフをレポート上で操作し見られるまでを作成できました。

ここまで理解できたなら、はじめの一歩は十分に踏み出せたと思います。

また、今回用意した練習用データには他の項目もあります。ぜひこれらを使用して別のグラフも作成してみてください。

例えば他にも、以下のような観点でレポートが作れそうです。

  • 店舗別の売上はどうなっている?
  • 売上の中でそれぞれの品目が占める割合はどれくらいだろうか?
  • どの品物が最も売上が多い?販売数は?
  • 売上の中でリピーターはどれくらいいるだろうか?各店舗では?

LookerStudioはじめの一歩サンプルデータ(スプレッドシート)

本記事が、 Looker Studio を使い始めたい誰かの役に立てたら幸いです。


現在 back check 開発チームでは一緒に働く仲間を募集中です。 herp.careers

PHP 8.3 の新機能を試してみよう

この記事は個人ブログと同じ内容です

www.ritolab.com


PHP 8.3 が 2024 年 11 月にリリースになります。PHP 8.3 の新しい機能をいくつかピックアップして試してみます。

クラス定数の型指定

クラスで定義する定数に型の指定が行えるようになりました。

Typed class constants
https://wiki.php.net/rfc/typed_class_constants

<?php

class Person {
    const string NAME = 'rito';
}

指定した型ではない値を代入するとエラーになります。

<?php

class Person {
    const string NAME = 777;
}

// Fatal error: Cannot use int as value for class constant Person::NAME of type string in...

クラス定数の動的参照

Dynamic class constant fetch
https://wiki.php.net/rfc/dynamic_class_constant_fetch

クラスで定義した定数を、変数で動的に参照できるようになりました。

<?php

class Person {
    const string NAME = 'rito';
}

$personNameConstant = 'NAME';

echo Person::{$personNameConstant};
// => rito

PHP 8.2 までは constant() 関数を使わないと同様のことはできませんでした。

<?php

echo constant("Person::$personNameConstant");
// => rito

8.3 でも constant() 関数は引き続き使えます。

Override attribute によるオーバーライド指定

Marking overridden methods (#[Override])
https://wiki.php.net/rfc/marking_overriden_methods

PHP は、実装されたメソッドのシグネチャが指定されたインターフェイスまたは親クラスからオーバーライドされたメソッドと互換性があることを検証しますが、メソッドが実際にインターフェイスのメソッドを実装することを目的としているのか、親メソッドをオーバーライドすることを目的としているかどうかの「意図」を確認することができません。

例として、以下の基底クラスと派生クラスがあったとします。派生クラス側で、基底クラスのメソッドをオーバーライドしています。

<?php

class Book {
    protected function setName(string $name): void { /* some processing... */}
}

class PhpReference extends Book {
    // setName() をオーバーライドしている
    public function setName(string $name): void { /* some processing... */}
}

このとき、基底クラス側のメソッド名が変更されたらどうなるでしょう。

<?php

class Book {
    // protected function setName(): void { /* some processing... */}
    // ↓ メソッド名を変更した
    protected function setTitle(string $name): void { /* some processing... */}
}

class PhpReference extends Book {
    // setName() をオーバーライドしていたはずが、基底クラス側のメソッド名が変わったために派生クラス独自のメソッドとなってしまった
    public function setName(string $name): void { /* some processing... */}
}

基底クラス側のメソッド変更によって、派生クラス側のオーバーライドが独自メソッドになってしまいます。つまり、PhpReference クラスは setTitle() 関数も、setName() 関数も動作することになります。

PHP はこういった「意図」の検出ができません。

PHP 8.3 では、Override アトリビュート(#[\Override]) をつけることによって、そのメソッドがオーバーライドであることを示せるようになりました。 これによって、基底クラス側のメソッド名変更があった時にエラーにすることができます。

<?php

class Book {
    protected function setName(): void { /* some processing... */}
}

class PhpReference extends Book {
    #[\Override]
    public function setName(): void { /* some processing... */}
}

基底クラス側でメソッドの変更が行われた場合はエラーが発生します。 

<?php

class Book {
    // protected function setName(): void { /* some processing... */}
    // ↓ メソッド名を変更した
    protected function setTitle(): void { /* some processing... */}
}

class PhpReference extends Book {
    #[\Override]
    public function setName(): void { /* some processing... */}
    // => Fatal error: PhpReference::setName() has #[\Override] attribute, but no matching parent method exists in...
}

INI ファイル環境変数設定におけるデフォルト値設定

php.ini での環境変数設定でデフォルト値が設定可能になりました。

INI ファイル内では環境変数が参照できるため、これと併用することでデフォルト値の運用が可能になります。

まずは PHP 8.2 までの挙動を見てみます。

post_max_size = ${POST_MAX_SIZE}
<?php

/*
 * 〜 PHP 8,2
 */ 

// php.ini
// post_max_size = 8M
echo ini_get('post_max_size');
// => 8M

// 環境変数 POST_MAX_SIZE が設定されている
echo ini_get('post_max_size');
// => 10M

// 環境変数 POST_MAX_SIZE が設定されていない
echo ini_get('post_max_size');
// => ''

PHP 8,2 までは、値が指定されていない場合は未設定となり、値を参照すると空文字が返ります。

PHP 8.3 では、デフォルト値を指定しておけば、環境変数の設定が無い場合もデフォルト値を適用してくれます。

post_max_size = "${POST_MAX_SIZE:-6M}"
<?php

/*
 * PHP 8,3 〜 
 */ 

// 環境変数 POST_MAX_SIZE が設定されている
echo ini_get('post_max_size');
// => 10M

// 環境変数 POST_MAX_SIZE が設定されていない
echo ini_get('post_max_size');
// => 6M

json_validate() 関数

json_validate() 関数が追加されました。

json_validate
https://wiki.php.net/rfc/json_validate

この関数は、文字列が有効な json 文字列であるかどうかを検証します。

これまでは、json_decode() 関数 の結果によってハンドリングすることが多かったはずです。

<?php

$json = '';

try {
    json_decode(json: $json, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    /* some processing... */
}

ただしこの場合、文字列の解析で ZVAL(object, arrayなど) を生成するため、メモリを消費する上、メモリ保存処理分のオーバーヘッドも発生します。

対して PHP 8.3 で追加された json_validate() 関数はこれらの処理を行わないため、リソース節約・速度向上に貢献します。

また、json_validate() 関数で使用する Parser は json_decode() 関数と共通のため、json_validate() が通れば json_decode() も成功する。ということが担保されます。

json_validate( string $json, int $Depth = 512, int $flags = 0 ) : bool
<?php

$json = '';
json_validate($json);
// => false

$json = '{"a":"aaa","b":"bbb"}';
json_validate($json);
// => true

まとめ

PHP 8.3では、クラス定数の型指定やオーバーライド指定、そして json_validate() 関数など、うれしいアップデートがありました。

今回取り上げたものは一部です。新機能は他にもあるのでチェックしてみてください。

https://wiki.php.net/rfc#php_83


現在 back check 開発チームでは一緒に働く仲間を募集中です。 herp.careers

国作りワークショップで見つけた、日常の小さな変化をシェアして安定的な成長環境を構築する心得

この記事は個人ブログと同じ内容です

国作りワークショップで見つけた、日常の小さな変化をシェアして安定的な成長環境を構築する心得

※この記事のタイトルは XP 祭りのセッション「あなたも元高角三(げんこうかくぞう)になろう!- 文章力で世界を変革する技術」のワークショップで学んだコピーライティングを実践した結果できたタイトルです。少々大袈裟かもですが、優しい目でお気楽に読み流していただけると幸いです。

こんにちは、back checkの開発をしておりますぐっきーです。 今回はXP祭りのオフラインの国作りワークショップに参加してきたのでイベントレポートとして本記事を書きます。

confengine.com

内省と周りの人との関係構築を促すワークショップ

まず @ryo_endo さんが考案された「 国作りワークショップ」の概要を説明します。

・国づくり ・国際交流

上記の2つのメタファーを通して自分自身の価値観や自分とは異なる価値観の理解を深めていくことを目的としたワークショップと紹介されていました。

進行

参加人数:約10名

参加者はテーマとしてあげられたいくつかのお題(今回は「問題解決」「関係性」「自己成長」)に対して自分の価値観として最も大切にするものを選択します。 価値観を選択した参加者は、同じ価値観を選択した他の参加者とグループになって国作りをしていきます。

国作りの手順は以下となります。

  1. なぜ自分がその価値観を選択したのかグループメンバーと共有する。
  2. グループ内のメンバーで協力し、国の国旗とその国特有の挨拶を作成する。

その後、他の国(別の価値観を選択したグループ)と国際交流を行います。

国際交流の流れ

  1. 国に観光にきた人へのおもてなしの準備を行います。ここでは各グループの中で自国に残り、他国からの観光客から自分達の国について投げられた質問に回答する人を選出します。
  2. 自国に残らないメンバーは他の国に観光に行きます。ここでは自国に残らないメンバーが他国のテーブルへいきます。
  3. 他国へ観光にいったメンバーは、国旗とその国の挨拶をはじめその国に関するちょっと意地悪な質問を投げます。質問に対しておもてなしをするメンバーは自国への質問に対して観光大使として回答します。
  4. 他国へ観光にいくメンバーはタイムボックスの間自国以外を行き来し、それぞれの国を知るための質問を繰り返します。
  5. タイムボックス終了後、自国に残ったメンバーと他国へ観光にいったメンバーを入れ替え、全ロールを経験するまで繰り返します。

日常の服を脱ぐ

印象的だったポイントとして、最も大切にする価値観を選択する流れで、最初に価値観を選んだ後ファシリテーターから「仕事のことを考えて価値観を選んでいませんか?」という問いかけがありました。その上で服を脱ぐ所作をするワークを行い「ユニークな服を着ていた人はいませんか?」など服を脱ぐワークに集中するような場作りがありました。

このワークを行った後に再度「服を脱いだ後、あなたは改めてどの価値観を大切に考えますか?」という問いかけを受けて参加者は再度価値観を選択しました。

この全員で体を動かして服を脱ぐワークを挟んだことによって、参加者間で自己主張に対する心理的安全性が高まり、その後の自分が選択した価値観をワークに参加した周囲のメンバーに共有することに対して抵抗がなくなったように感じました。 また、自分が最初に選択した価値観から1度考えをそらして再度選択肢に上がった価値観について見つめ直すことにより、ふりかえり手法の「5つのなぜ」のように自分の価値観に対して一段深く考えられたように思います。

ちなみに私は参加者として、最初の選択では「関係性」を選びました。これは自分自身が素敵だと思う人が集まるコミュニティとふれあうことが自分の潜在的な欲求だと考えたからです。しかし、服を脱いだ後に再度選択した価値観は「自己成長」でした。この大切に思う価値観の変更に至った理由は、自分が時間を共有したいと考えるコミュニティと出会うまでには必ず私の中で未知の領域の探索によって新しいコミュニティと出会ってきたからです。この未知の探索が「自己成長」のプロセスではないかと考えたため、最初の選択とは異なる価値観を選択しました。

自分達による価値観の可視化

次に国作りのワークで行った国旗づくりと挨拶づくりで感じたことを紹介します。

国旗づくりと挨拶づくりでは、事前にグループ内でなぜ自分がその価値観を選択したのかを共有し、その後グループメンバーの中で「自己成長」のイメージに対するキーワードを探しました。 私たちのグループでは、自己成長のイメージとして「頑張る」「周囲の人へのポジティブな影響」「手放す(周りの人を信じて託す。自分ができるようになることで周りの人達もできるようになっていく。育てて放流し、また次の世代に繋げていく。これらを自己の成長によって実現するようなイメージ)」が上げられました。これらのキーワードを元にインクリメンタルなサイクルが中心から生まれどんどん派生し増えていくようなデザインの国旗を作成しました。

また挨拶作りでは、サイクルがインクリメントしていく様子を表すジャスチャーを取り入れ、ヘリコプターのプロペラのように手を回す動作を取り入れました。 ちなみに私のニックネームを口に出した際の響きの良さがウケたことで、挨拶は「グッキー」に決定しました。 ※会場で参加者皆が「グッキー」を連呼するのはめちゃくちゃ恥ずかしかったです。その後懇親会まで引っ張られる程度には浸透しました。

国旗づくりと挨拶づくりのワークによって、なぜ自分自身が「自己成長」が重要だと感じたのか。また、その感覚の芯の部分にはどのような考えがあるのかが整理できたように思います。

周りの人からの問いによる価値観の深掘り

国際交流のワークでは、他国のテーブルに観光に行き、その国の国旗と挨拶をはじめ、その国に関するちょっと意地悪な質問を投げました。 ちょっと意地悪な質問とは、無茶振りのように「あなたの国ではどんな遊びが流行っていますか?」など国旗と挨拶に関する単純な質問よりもイメージを膨らませないと回答できないような質問を指します。

価値観をぶらさずに場面毎に適用するならなにが必要か?を即興で考えるのが難しかったですが、価値観を促進するアクションの具体がイメージできました。

以下に印象的だった質問とその回答を紹介します。

  1. 「自己成長国ではどんな遊びが流行っていますか?」

  2. いつもとちょっとだけなにかを変えてみる。例えばいつもの自分と違う服の着方をしてみる。新しい価値の発見を皆が楽しんでいる文化がある。

  3. 「自己成長国のおすすめのお店を教えてください」

  4. シェア屋さん。お客さんの興味をそそるような様々な領域の変化をパッケージ化して提供してくれる。(それって情報商材じゃないですか?というツッコミもあったり)

  5. 「自己成長国ではなにをしたら逮捕されますか?」

  6. 個人の成長、変化を強制しない。押しつけない。過去自己成長国が独裁政権だった時代は横行していたが、今の国では禁止し厳罰の対象としている。現在は自己成長国では変化は自由である。他人と変化量を比較することは犯罪ではないが、マナー違反でありこれをする人は紳士的でないと考えられている。

※自己成長国: 自己成長の価値観を選んだグループの国名

小さな変化をシェアして安定的な成長を実現する

ここまで一連の流れを終えた後、自分の価値観に対する発見をまとめ共有する時間がありました。

このワークショップ全体を通しての発見を私は下記のようにまとめました。

心得

まずは個人の範囲内で自己成長を楽しもう。望まない人には押し付けない。

成功も失敗も「変化」があることで得られた学びとしてポジティブに捉えよう。

変化を積極的にシェアしよう。様々な領域の変化掛け合わせで誰か(自分も含め)の役に立つパッケージを作ろう。

変化に「気づく」ようになろう。変化に気づいたら、相手に変化したねと伝え賞賛しよう。

これらを総じて振り返った中で自分達の考えていた「成長」とはなにか?という問いに対して、自分達の考えていた成長とは「変化」であるという結論に至りました。 行動によって生じるちょっとした変化。be agile を志すものとして、日常の小さな変化を捉え、言語化し、シェアすることで安定的な成長(変化)の環境を実現できるのではないでしょうか。

公開ほやほや Laravel Eloquent 用の日時フィルタリングパッケージ Laravel Date Filtering をお試し

この記事は個人ブログと同じ内容です

www.ritolab.com


Laravel Date Filtering という、Eloquent でのデータ取得における日時フィルタリングを提供するパッケージを試してみます。

Laravel Date Filtering

Laravel Date Filtering は、Laravel Eloquent モデル向けの、日付に基づく高度なフィルタリングと操作を簡素化するパッケージです。さまざまな日付と時間の間隔に基づいてレコードをフィルタリングするための便利なメソッドセットを提供します。

https://github.com/omarelnaghy/lara-date-filter

インストールと初期設定

パッケージをインストールします。

composer require omar-elnaghy/laradate-filters

設定ファイルを publish します。

php artisan vendor:publish --provider="OmarElnaghy\LaraDateFilters\ServiceProvider"

config/lara_date_filter.php が生成されます。

<?php
return [
    'custom_date_filter_convention' => [
        'get{duration}{unit}Records',
        'get{duration}{unit}Records',
        'get{duration}{unit}Records',
    ],
];

日付をフィルタリングするメソッド FilterByDateRange, FilterByDateHoursRange, FilterByDateMinutesRange などを使用可能にするため、対象のモデルの newEloquentBuilder() メソッドをオーバーライドしますします。

今回は User モデルで実装していきます。

use Illuminate\Database\Eloquent\Model;
use OmarElnaghy\LaraDateFilters\Traits\Builder\PackageBuilder;

class User extends Model
{
    .
    . (略)
    .
    
    
    /**
     * @param QueryBuilder $query
     * 
     * @return PackageBuilder
     */
    public function newEloquentBuilder($query): PackageBuilder
    {
        return new PackageBuilder($query);
    }
}

オーバーライドはしたものの、結果として Builder を extends し本パッケージ用のメソッドを Traits で付与したクラスを返しているため、ベースである Builder クラスに変更はありません。

これでこのパッケージを使い始める準備が整いました。

User データ

これからフィルタリングを行っていきますが、 今回使用しているデータは、 created_at が 2023-09-01 〜 2023-09-30 までの 30 日の範囲で 1 名ずつ User を作成した全 30 件のデータで行っています。

created_at は datetime で収録していますが、時間は全て 00:00:00 です。

フィルタリング:filterByDateRange

ここからは、実際のフィルタリングを見ていきたいと思います。

Laravel Date Filtering の基本メソッドである filterByDateRange() メソッドを使ってみます。

$startDate = Carbon::parse('2023-09-03');
$duration  = 2;
$dateUnit  = 'day';
$range     = DateRange::INCLUSIVE;
$direction = 'after';

// User をフィルタリング
$users = User::filterByDateRange($duration, $dateUnit, $startDate, $direction, $range)->get();

各項目を解説する前に、上記条件のフィルタリングによって得られた結果を先に共有しておきます。

// $users(配列に変換し必要なフィールドのみに限定しています。実際には\Illuminate\Database\Eloquent\Collection<int, \App\Models\User> が返ります。)
Array
(
    [0] => Array
        (
            [id] => 3
            [created_at] => 2023-09-03 00:00:00
        )

    [1] => Array
        (
            [id] => 4
            [created_at] => 2023-09-04 00:00:00
        )

    [2] => Array
        (
            [id] => 5
            [created_at] => 2023-09-05 00:00:00
        )

)

filterByDateRange メソッドの各引数は以下の意味とバリエーションになります。

/**
 *  @param int $duration
 *  @param string $dateUnit
 *  @param Carbon $date
 *  @param string $direction
 *  @param DateRange $range
 */
public function filterByDateRange(int $duration, string $dateUnit, Carbon $date, string $direction = 'after', DateRange $range = DateRange::INCLUSIVE)
  • $duration
    • 範囲の数値
  • $dateUnit
    • $duration の単位
      • 'second'
      • 'minute'
      • 'hour'
      • 'day'
      • 'week'
      • 'month'
      • 'year'
  • $date
    • 基点日
  • $direction
    • 基点日から過去・未来どちらの方向へ範囲を取るか
      • 'before'(過去)
      • 'after'(未来)
  • $range
    • 範囲指定における、起点、及び終点を含めるか
      • INCLUSIVE(含まれる)
      • EXCLUSIVE(含まれない)

$duration = 1, $dateUnit = 'day' なら、 1 日間ということになります。

この時、$duration = 0 なら、起点日のみという扱いになります。

また、$range が少しややこしいので説明しておくと、

$startDate = 9/3, $duration = 2, $dateUnit = 'day', $direction = 'after'

とした場合、DateRange::INCLUSIVE なら

9/3 <= targets <= 9/5

となり、9/3, 9/4, 9/5 のデータが返されます。

DateRange::EXCLUSIVE の場合は

9/3 < targets < 9/5

となり、9/4 のデータが返されます。

https://github.com/omarelnaghy/lara-date-filter/blob/6b69c9fe1695ecba77786c0c7c698269fb23d274/src/Traits/BuilderTrait.php#L21

クエリビルダとの違い

ここで、Laravel Date Filtering を用いた最初の実装を再度見てみます。

$startDate = Carbon::parse('2023-09-03');
$duration  = 2;
$dateUnit  = 'day';
$range     = DateRange::INCLUSIVE;
$direction = 'after';

// User をフィルタリング
$users = User::filterByDateRange($duration, $dateUnit, $startDate, $direction, $range)->get();

このときに、同じ実装をクエリビルダで行うとどうなるか見てみます。

日付範囲を内包する場合

Laravel Date Filtering でいうところの $rangeDateRange::INCLUSIVE の場合です。

$startDate = CarbonImmutable::parse('2023-09-03');
$duration  = 2;
$endDate   = $startDate->addDays($duration);

$users = User::whereBetween('created_at', [$startDate, $endDate])->get();

クエリビルダでも割合シンプルに書けそうです。では日付範囲を内包しない場合はどうでしょうか。

日付範囲を内包しない場合

Laravel Date Filtering でいうところの $rangeDateRange::EXCLUSIVE の場合です。

$startDate = CarbonImmutable::parse('2023-09-03');
$duration  = 2;
$endDate   = $startDate->addDays($duration);

$users = User::where(function (Builder $query) use ($startDate, $endDate) {
    $query->whereDate('created_at', '>', $startDate)
          ->whereDate('created_at', '<', $endDate);
})->get();

若干記述するコード量は増えましたが、まあ書けなくはなさそうです。

Laravel Date Filtering とクエリビルダでの日付フィルタリング

2 つを比べてみると、記述量にさほど違いはないものの、クエリビルダの場合は日付範囲の条件によって Builder の組み立て自体が変化します。

対して Laravel Date Filtering パッケージの場合は、そこを引数で吸収しているため、日付範囲の条件によって式を変えなくても良いという利点がありそうです。

その他のフィルタリングメソッド

filterByDateRange メソッドの他にも、以下のメソッドが提供されています。

これらは filterByDateRange メソッドにおける $dateUnit の省略版になっています。

Custom Eloquent Builder

Laravel Date Filtering ではカスタム Eloquent ビルダーを提供しており、メソッドの名前を "filterByDateXRange" パターンに従って命名すると、動的にメソッドを生成し、実行します。

// [Duration] は検索したい [日付単位(Date Unit)] の数を示す数値
return Post::filterByDate(Duration)(Date Unit)Range($startDate, $direction, $range)->get();

例えば、以下のようなメソッドが任意で組み立て可能です。

Post::filterByDate3DayRange($startDate, $direction, $range)->get();

Post::filterByDate3WeekRange($startDate, $direction, $range)->get();

Post::filterByDate6MonthRange($startDate, $direction, $range)->get();

created_at 以外のカラムでフィルタリングするには

(あくまでも 2023 年 9 月 22 日時点です)

Laravel Date Filtering のフィルタリングは、基本的に created_at を見るようになっています。

created_at 以外のカラムでフィルタリングする、 ないし動的にカラムを指定する方法は提供されていませんでした。

モデルに public $dateColumn = 'updated_at'; と指定すればフィルタリングのカラム変更はできるものの、あまり実用的ではないため、このあたりの開発はまだこれからのようです。

今後に期待の楽しみなパッケージ

本記事執筆が 2023 年 9 月 22 日ですが、Laravel Date Filtering は first commit が 2023 年 9 月 13 日と、まだ公開されて間もないパッケージです。

日付での複雑なフィルタリングが頻発するアプリケーションでは重宝しそうなパッケージなので、今後の機能拡充に期待しています。

https://github.com/omarelnaghy/lara-date-filter


現在 back check 開発チームでは一緒に働く仲間を募集中です。 herp.careers

ROXX 社内の開発組織向けラジオの取り組み紹介

この記事は個人ブログと同じ内容です

ROXX 社内の開発組織向けラジオの取り組み紹介

こんにちは、株式会社ROXX でエンジニアをやっている ぐっきーです。

今回は最近始めた開発組織内の交流イベント ROXX DevRadio についてご紹介したいと思います。

この記事はこんな方に向けた内容を発信しています。 - 自社を盛り上げたいが、どんなことやったらいいか迷っている方 - ROXX の開発組織に興味を持っていただいている方 - ROXX の開発組織に在籍している方

ROXX DevRadio について

ROXX DevRadio の様子

概要

社内の気になるメンバーをお迎えして深掘りするラジオ形式のイベントです。 - 主にゲストの方のパーソナリティと開発文化に関わる内容を中心に扱う - 約30分のトークセッション形式 - 収録した動画はイベント後に社内の slack で共有

リアルタイムでの公開収録 - リスナーの方にもお昼たべながら参加して聞き専もよし、わいわい盛り上げてもよしスタイルで運営しております。

取り組みの背景

元々ROXXの中で agent bank, back check という2つの事業部(現在は Records 事業部、 CTO 室、コーポレートITが追加)に対してそれぞれ開発組織がありましたが、相互の交流はほとんどない状況でした。 そのためプロジェクト間の経験や知識の共有がしづらい雰囲気がありました。

そこで CTO の kotamat さんが当時旗振りをしてくれつつ、社内の各セクションの交流を増やすという目的で全員参加で毎月 Meetup イベントを開催しておりました。

例えばこんなのとか - 社内で TDD のワークショップを開催しました! - RSGT2023をチームに持ち帰るための取り組み紹介 - GPTを使って1hでアプリ作ってみよう - etc...

しかし1年ほど継続した結果、開発メンバーが増えたことにより全員参加が難しくなったり、企画や LT 準備に負担を感じることがあったりでだんだんと運営自体が重くなってきたという課題感がありました。

そこで解決策として、「重すぎないイベント運営によって、社内のDev組織間・メンバー間で、カジュアルに情報交換・交流が行える場を提供する」というコンセプトでまずは「知っている人」を増やすという目的から DevRadio の取り組みを始めることとしました。

運営体制の紹介

kotamat さんから運営委員を引き継ぎ、社内の有志(たまたま各部署から1名ずつ)の4人 + プロダクト開発部 GM の宮竹さんのサポートの体制で運営しています。

運営準備など裏側でやっていること

運営側もつらくないことを意識して開催フォーマットを型化することで、ふりかえりから事前準備含めて約 1 時間ちょっとの工数で準備ができています。(これ大事)

  • ふりかえり: 30分
  • ゲスト決め
  • ゲストへの参加依頼
  • show note づくり: 30分
  • 事前、事後アンケート
  • 社内告知

過去回の紹介

第 1 回

ゲスト:高畠 正和さん ポジション:agent bank プロダクトマネージャー 話した内容:

- 地元、北海道話
- 営業→エンジニア→PdM。何があった?
- とっておきの失敗話
- 今後の野望があれば...

第 2 回

ゲスト:三浦 史也さん ポジション:コーポレートIT 話した内容:

- ROXXで(音楽関係で)やりたいこと
- 実際セキュリティ監視やセキュリティ診断って何やってるの?
    - どんなインシデント対応やログ調査をやってきたか
    - 今後ROXXでどんなことをしていきたいか?
    - つらかったこと、つらかった現場ばなし
- プロダクト開発部の人達との接点、またどのように関わっていけるのが理想的か

第 3 回

ゲスト:竹原 駿平さん ポジション:back check テックリード 話した内容:

- ROXX 来る前の話
- ROXXでテックリードとして働くまでのキャリアの話
    - 技術キャッチアップを高度にしていくための工夫ってある?
- 普段からやってる積み上げ、個々の場面で意識していることは?
- 子育て、スプラ、いつやってんの?

参加してくれた方の声

参加したくれた方達の感想としても下記のようにポジティブなお声をいただけております!

  • リスナーの皆さん
    • 内容が濃くて超面白かったです!
    • 時間の長さもちょうどよく聞きやすかった。
    • めちゃくちゃ良かったです! 丁度仕事がたまってたんですが、こういう形式だったので参加することができました。
  • ゲストの皆さん(依頼を投げたら皆さん、快く引き受けていただき大変に感謝です)
    • 自分自身のこれまでを言語化する良い機会でした。
    • ゲストとして参加して楽しかった。
    • 事前準備の負担もさほどなかった。

リスナーの方向け、おすすめの楽しみ方

これは社内のメンバーが読んでくれたらうれしいなと思い添えておきます。

  • お気軽に収録のMTGに参加、リアクション等を投げて参加しつつ楽しんでいってください。
  • 作業の集中時間の BGM としてご活用ください。
  • ○○さん、DevRadio でこんなこと話してたよね〜など社内のメンバー間での雑談のネタとしてご活用ください。

運営として、参加してくれた方にこんなことしていただけたら喜びます。

  • △△さんのこんな話聞いてみたいなどのご要望をいただけると嬉しいです!
  • 皆さんの方から「最近やってるこんなこと紹介したいからゲストとして呼んでほしい」とか声かけてくれたらめちゃめちゃ嬉しいです。
  • 運営として DevRadio はじめイベント企画などを通して ROXX の開発組織を一緒に盛り上げたいという方がいたら声をかけてくれたら最高にうれしいです。

今後の意気込み

ROXX はいま今後のスケールに向けて、開発組織に面白い人達がどんどん入ってきているフェーズです。 運営メンバーの考えとして、多様な背景や考え方、スキルセットの掛け合わせがより多く生まれるほど、働く人にとっても顧客にとってもわくわくするような体験を提供できる機会が創出できると信じています。

今後もイベント企画等を通して組織の横の繋がりを拡大することで、業務におけるコラボレーションのハードルをどんどん下げていくような取り組みをやっていく予定です。

もし ROXX の開発組織やイベント運営にご興味を持っていただけた方がいらしたら、ぜひお気軽にお声がけください。

Twitter: Gukki- ぐっきー_@Area029S

時系列分析による時系列データの解析と未来予測(ARIMA, SARIMA)

この記事は個人ブログと同じ内容です

www.ritolab.com


時系列分析の基礎を確認しつつ、「データの確認・理解」「定常データへの変換」「モデル構築(ARIMA, SARIMA)」と一連の時系列分析の流れを実施し、時系列データの未来予測を行っていきます。

時系列分析とは

時系列分析は、時間的な順序で取られたデータ(=時系列データ)の特性やパターンを分析し、過去の振る舞いから将来の振る舞いを予測するための統計的手法です。

時系列データとは

時系列データは、一定の時間間隔(日次、週次、月次など)で観測されたデータポイントから構成されるデータです。

このような時系列データは、経済・株価・気象・トラフィックデータなど、多くの実世界の現象を表現するために使用されます。

  • ある地域の毎日の気温
  • ある店舗の日次の売上

通常の時系列データでは観測者によって観測の時間間隔が設定されます。

時系列データの特徴

  • 時系列データは一度しか観測されない
  • 観測値から平均や分散などを推定することはできない
    • 時間の非独立性
      • 時系列データの観測値は時間的に依存しており、過去の値が現在の値に影響を与える可能性がある。したがって、単純な平均や分散の推定では、データの時間的なパターンや相関関係を考慮することができない。
    • 季節性や周期性
      • 時系列データには季節性や周期性が存在する場合がある。これらの要素は平均や分散に影響を与える可能性があり、単純な統計的手法では適切にモデリングできない。
    • トレンド
      • 時系列データには長期的なトレンドが存在することがある。トレンドは平均値の変動を引き起こし、推定結果に影響を与える可能性がある。

時系列データとは区別するべきデータ

地震観測データや為替取引の Tick データは、「点過程(ポイントプロセス)データ」または「マーク付き点過程データ」と呼ばれます。これらのデータは、発生時間と発生間隔に意味があり、観測者が発生間隔を設定できません。

これらも時系列分析の対象となり得ますが、通常の時系列データとは異なる特性を持つため、それに応じた解析手法やモデルの適用が必要となります。

時系列分析で出来ること・わかること

時系列分析では、以下について知ることができます。

  • トレンド
    • 時系列データに現れる長期的な変化や傾向(トレンド)を把握できます。トレンドの有無や方向、変動のパターンを特定することができます。
  • 季節性
    • 時系列データに現れる季節的な変動を把握できます。特定の季節パターンや周期性を検出し、季節要素の影響を理解することができます。
  • 周期性
    • 時系列データに周期的な変動がある場合、その周期や周期の長さを特定することができます。サイクルの長さや振幅の変動を分析することができます。
  • 予測
    • 過去の時系列データを基に将来の値を予測することができます。予測モデルを構築し、将来の傾向や変動を推定することができます。
  • 異常値検出
    • 時系列データから異常値や外れ値を検出することができます。異常な振る舞いや予測モデルからの逸脱を特定し、異常値の原因や特徴を分析することができます。
  • 時系列データ間の相関関係
    • 他の時系列データや外部要因との関係を分析し、相互の影響や連動性を理解することができます。

これらの分析を通じて、時系列データの特徴や変動要因を理解し、将来の予測や意思決定に活用することができます。

時系列分析の手順

  1. データの特性の理解
    • 時系列データの基本的な特性を調査し、傾向(トレンド)、季節性、周期性、ランダム性などのパターンを特定。
  2. モデリング
    1. 時系列データにモデルを適用し、データの生成プロセスを表現するための数学的なモデルを構築。
    2. 代表的なモデル
      • ARIMA(Autoregressive Integrated Moving Average)
      • SARIMA(Seasonal ARIMA)
      • VAR(Vector AutoRegression)など
  3. 予測
    • 構築したモデルを使用し将来のデータポイントを予測。
  4. モデルの診断と改善
    • 構築したモデルの適合度や残差の診断を行い、モデルの改善や修正を行う。モデルの信頼性と予測精度を向上させていく。

サンプルデータ

今回は、航空機の乗客数データを使って未来の予測や季節性などの理解を行っていきます。

その際に、ARIMA モデル、及び SARIMA モデルを作成します。

R 言語の標準データセットとして提供されている「AirPassengers」を利用しますが、今回は Python で進めるため、Kaggle で AirPassengers データセットをダウンロードしておきます。

Air Passengers - Kaggle

データの読み込みと調整

ライブラリの読み込みを行い、データを読み込みます。

import pandas as pd
import numpy as np
import statsmodels
import statsmodels.api as sm

from matplotlib import pylab as plt
import seaborn as sns
%matplotlib inline
sns.set()

from matplotlib.pylab import rcParams
rcParams['figure.figsize'] = 16, 8

# データ読み込み
df = pd.read_csv('AirPassengers.csv')

読み込んだデータを確認します。

# df

    Month   #Passengers
0   1949-01 112
1   1949-02 118
2   1949-03 132
3   1949-04 129
4   1949-05 121
... ... ...
139 1960-08 606
140 1960-09 508
141 1960-10 461
142 1960-11 390
143 1960-12 432


# df.dtypes

Month          object
#Passengers     int64
dtype: object

データを使いやすくするためにカラム名をリネームし、Month をインデックスにしておきます。

## カラム名リネーム
df = df.rename(columns={'#Passengers': 'Passengers'})
## datetime 化
df['Month'] = pd.to_datetime(df['Month'] )

# Month をインデックスにする
df.set_index('Month', inplace=True)

再度データを確認します。

# df

      Passengers
Month   
1949-01-01  112
1949-02-01  118
1949-03-01  132
1949-04-01  129
1949-05-01  121
... ...
1960-08-01  606
1960-09-01  508
1960-10-01  461
1960-11-01  390
1960-12-01  432
144 rows × 1 columns


# df.dtypes

Passengers    int64
dtype: object

Month をインデックスに変換したのは、整列・集計・プロットなどを行いやすくするためです。

データの確認

データの読み込みと整理が済んだので改めてデータを見ていくと、1949 年 1 月から、1960 年 12 月までの乗客数データが収録されています。 プロットして見てみます。

plt.plot(df['Passengers'])

視覚的には、どことなく似たような周期のアップダウンを繰り返しながら上昇しているように見えます。

この時系列データが非定常性を持つかを数値的に判断するために、ディッキーフラー検定を実施してみます。

Dickey-Fuller 検定(ディッキーフラー検定)

ディッキーフラー検定は、自己回帰モデルにおける単位根の有無の検定です。

  • 帰無仮説:「データ系列に単位根が存在する」
  • 対立仮説:「データ系列は定常性を有す」

時系列データの平均、分散、自己相関係数などの計算をはじめとした時系列データの分析をする際には、事前に検定を行い、分析対象の時系列データが定常性を有するか確かめることが必要です。チャートを見ると非定常性が明らかな感じがしますが、見た目だけ判断せず検定を行って確認をしておきます。

単位根

単位根(unit root) は、時系列データの性質を表す統計的な概念です。単位根を持つ時系列データは、長期的なトレンドや構造的な変化が存在し、平均値が一定でない(恒久的にランダムウォークをするような特性を持っている)ことを示します。単位根は統計的仮説検定によって検出され、データの非定常性を考慮する必要があります。

つまり、Dickey-Fuller 検定によって帰無仮説が棄却されない場合、単位根が存在すると判断し、その時系列データは非定常性である。と結論づけられます。

非定常性

非定常性(non-stationary) とは、時系列データの統計的性質が時間に依存し、一定のパターンや特性を持たない状態を指します。非定常性を持つ時系列データは、平均や分散が時間の経過とともに変動する傾向 があります。

非定常性を示す時系列データには、以下のような特徴があります:

  • トレンド(Trend)
    • データが長期的に上昇または下降する傾向がある場合、トレンドが存在します。トレンドは、統計的に見て平均値が時間とともに変化することを示します。
  • 季節性(Seasonality)
    • データに周期的なパターンや季節的な変動がある場合、季節性が存在します。季節性は、統計的に見て周期的な変動があることを示します。
  • 周期性(Cyclicity)
    • データに定期的な周期性があり、季節性とは異なる場合、周期性が存在します。周期性は、統計的に見て一定の期間で変動が生じることを示します。
  • 自己相関(Autocorrelation)
    • データの過去の値との相関関係があり、現在の値が過去の値に依存する場合、自己相関が存在します。自己相関は、統計的に見てデータが時間的な依存関係を持つことを示します。

非定常性を持つ時系列データは、定常性の仮定を満たさないため、統計モデリングや予測の精度を下げる可能性があります。そのため、非定常性のデータを扱う場合は、定常性を復元するための前処理や変換が必要となる場合があります。例えば、トレンドの除去や差分化、季節性の調整などが一般的なアプローチとして使用されます。これにより、データの統計的性質が一定である定常な状態に変換され、より正確な予測やモデリングが可能になります。

ディッキー・フラー検定の実施

Python では statsmodels の adfuller 関数で実施します。

statsmodels.tsa.stattools.adfuller

# ディッキー・フラー検定の実施
result = sm.tsa.stattools.adfuller(df['Passengers'])

# 結果の表示
print('ADF統計量(ADF Statistic):', result[0])
print('p値(p-value):', result[1])
print('臨界値(Critical Values):')
for key, value in result[4].items():
    print('\t%s: %.3f' % (key, value))

結果出力

ADF統計量(ADF Statistic): 0.8153688792060421
p値(p-value): 0.9918802434376409
臨界値(Critical Values):
    1%: -3.482
    5%: -2.884
    10%: -2.579

ディッキー・フラー検定の結果を解釈するためには、ADF 統計量と p 値を考慮し、臨界値と比較して帰無仮説を棄却するかどうかを確認します。

ADF 統計量は 0.8153688792060421 です。この値を臨界値と比較することで、時系列データの非定常性を評価します。臨界値は 1%、5%、および 10% の有意水準に対する値であり、以下のようになります

  • 1%: -3.482
  • 5%: -2.884
  • 10%: -2.579

結果を解釈するためには、ADF 統計量を臨界値と比較します。ADF 統計量が臨界値よりも小さい場合は定常性の存在が示唆され、帰無仮説を棄却できます。逆に、ADF 統計量が臨界値よりも大きい場合、定常性の存在が示されず、帰無仮説を棄却できません。

今回の場合、ADF 統計量の値(0.8153688792060421)は臨界値(-3.482、-2.884、-2.579)よりも大きいため、定常性の存在が示されません。また、p 値が非常に高い(0.9918802434376409)ため、帰無仮説を支持し、定常性の存在を示す証拠はほとんどありません。

したがって、与えられた結果からは「帰無仮説を棄却できない」ということになり、「データ系列に単位根が存在する」つまり「定常性を有するとは言えず、非定常性を有する可能性が高い」と結論付けられます。

非定常性・定常性を持つデータであるか

乗客数の原系列データ(未加工のもとのデータ)を見れば、年々数値が上昇(トレンドがある)していたり、そのトレンドの中でも似たようなアップダウン(周期性・季節性)があることがなんとなくわかると思います。

時系列分析におけるポイントとして、予測のためのモデル構築を行う際には、こういった「非定常データ」を「定常データ」という、トレンドや周期性・季節性を排除したデータへ変換してからモデルを構築する必要がある。という背景があります。

非定常データから定常データへの変換を行っていく過程で、変換後のデータが定常性を持つデータになっているかを確認するために、ディッキー・フラー検定は有効です。

時系列データの理解

続いて、今回の時系列データがどのような性質をもっているのかを確認していきます。

自己相関と偏自己相関を視覚化し、データの構造やパターンを理解します。

自己相関(Autocorrelation)

自己相関は、時系列データ内の異なる時間ラグ間の相互関係を測る統計量です。具体的には、ある時刻のデータと一定の時間ラグだけずれた時刻のデータとの間の相関を計算します。自己相関を調べることで、時系列データの周期性やパターンの特徴を把握することができます。

自己相関のグラフは、時系列データの過去の値と現在の値との間の相関関係を示します。このグラフを通じて、データがどれくらい自己相関を持っているか、周期性や季節性のパターンが存在するか、および他のタイプの相関関係があるかを視覚的に確認することができます。自己相関が高い場合、データが過去の値に依存しており、トレンドや周期性のパターンが存在する可能性があります。

また、自己相関、及び次に紹介する偏自己相関のグラフは一般に コレログラム(Correlogram) と呼ばれることもあります。

# 自己相関
fig = sm.graphics.tsa.plot_acf(df['Passengers'], lags=40)

x=0 の地点にある値が基点となるデータで、x=1 の地点にある値がラグ 1 となるデータです。つまり今回の乗客数データでは、x=0 が基点月で、x=1 が前月、x=1 が二ヶ月前で... という読み方ができます。

このコレログラムを見ると、 基点月と前月は強い正の自己相関を示しています。

また、全体的に一定のパターンで推移していそうなことも読み取れます。

グラフの青色の範囲は 95%信頼区間(=ACF{自己相関関数}の推定値が統計的に有意でないと考えられる範囲)を示してしますが、範囲を抜けているラグが 1 年くらいというのもあること、チャートの推移から1 年周期でのパターン(12の周期)があることが読み取れます。

偏自己相関(Partial Autocorrelation)

偏自己相関は、ある時刻と別の時刻の間で、他の時刻の影響を取り除いた相関を測る統計量です。つまり、一連の中間時刻を通じて制約されない2つの時刻間の直接的な相関を計算します。偏自己相関を調べることで、直接的な相関関係を評価することができます。

偏自己相関のグラフは、時系列データの過去の値と現在の値との間の直接的な相関関係を示します。このグラフを通じて、データがどの時点で自己相関を持つのか、他の時点の影響を排除した直接の相関関係が存在するかを視覚的に確認することができます。

# 偏自己相関
fig = sm.graphics.tsa.plot_pacf(df['Passengers'], lags=40)

0 地点のデータは 1 地点前のデータと強い正の自己相関にあり、前月の乗客数が多ければ当月も多くなることがわかります。

また、12 ヶ月周期の相関が見られるので、周期性ないし季節性があるとわかります。

和分課程〜非定常データから定常データへ

これらの時系列データから予測を立てられるようにするには、先ほど確認した時系列的な変動パターンがあると予測が立てられないため、周期変動を除去し、非定常から定常性のデータに変換していく必要があります。

時系列分析において、非定常データを定常データに変換することにはいくつかの重要な意味があります。

  • 定常性の仮定
    • 時系列データが定常である場合、データの統計的特性が時間に依存しなくなります。この特性は、時系列分析において重要な仮定です。定常データは、平均や分散が一定であり、自己共分散(自己相関)が時間に依存しないという特徴を持ちます。定常データである場合、統計的手法がより正確で信頼性の高い結果を提供しやすくなります。
  • モデルの安定性
    • 非定常データをそのまま使った場合、データの統計的特性が時間に依存しているため、モデルのパラメータが時間経過とともに変化してしまう可能性があります。これによって、予測の精度が低下したり、モデルの安定性が損なわれたりすることがあります。定常データに変換することで、モデルのパラメータを安定させることができます。
  • データ解釈と比較
    • 定常データは、時間に依存しない統計的特性を持つため、データの解釈や異なる時点のデータとの比較が容易になります。また、非定常データではトレンドや季節性の影響がデータ全体に影響を及ぼすため、異なる時点のデータを比較することが困難になることがあります。

非定常データを定常データに変換するために、和分過程(差分化)や季節調整などの方法が利用されます。これらの手法を用いることで、データの非定常性を取り除き、より信頼性のある分析や予測モデルの構築が可能となります。

モデルを構築する前に、これら時系列データの非定常性を除去していく様子を見ていこうと思います。

ちなみに、データの差分を取ることで非定常性(単位根)を除去した後の過程を 和分過程(Integrated Process) といいます。

差分系列(階差系列)

差分系列(階差系列, Difference series)は、時系列データの、連続する観測値間の差分を計算した系列(1時点離れたデータとの差をとったデータ)です。差分系列を計算することで、トレンドや季節性の影響を除去し、定常性を持ったデータに近づけることができます。

差分系列は数式で表すと以下で表現できます。

 \displaystyle
\Delta y_t = y_t - y_{t-1}

 y_t は時点  t における観測値を表し、y_{t-1} はその直前の時点  t-1 における観測値を示します。差分系列  \Delta y_t は、現在の観測値  y_t から直前の観測値 y_{t-1} を引いた値として計算されます。

Passengers(乗客数)の値から階差系列を出力します。

# 差分系列
df['difference'] = df['Passengers'].diff()
# df

    Passengers  difference
Month       
1949-01-01  112 NaN
1949-02-01  118 6.0
1949-03-01  132 14.0
1949-04-01  129 -3.0
1949-05-01  121 -8.0
... ... ...
1960-08-01  606 -16.0
1960-09-01  508 -98.0
1960-10-01  461 -47.0
1960-11-01  390 -71.0
1960-12-01  432 42.0
144 rows × 2 columns

一番はじめ、1949-01-01 の difference が NaN なのは、1 地点前のデータが存在しないためです。

差分系列をプロットしてみます。

plt.plot(df['difference'])

トレンドは除去されたようです。

しかし振れ幅がだんだん大きくなっていっているので分散はまだ一定にはなっていないことがわかります。

したがって、この時点では完全に定常性を持ったデータにはなっていないことがわかります。

対数系列(Log series)

対数系列は、時系列データの各観測値に対して対数変換を適用した系列です。対数変換により非線形な変動を緩和し、データの特性を正規分布に近づけることができます。

数式では以下で表現します。

 \displaystyle
\log  y_t = log(y_t)

 y_t は個々の時点  tにおける観測値や測定値を示し、 \log y_t y_t の自然対数を表します。この数式では、各時点の  y_t に対して自然対数を適用し、その結果を  \log y_t として表現しています。

定常性を持ったデータに近づけるため、こちらも試してみます。

# 対数系列(底が10となる対数を作成する)
df['log'] = np.log10(df['Passengers'])

対数系列データを確認します。

# df

Passengers  difference  log
Month           
1949-01-01  112 NaN 2.049218
1949-02-01  118 6.0 2.071882
1949-03-01  132 14.0    2.120574
1949-04-01  129 -3.0    2.110590
1949-05-01  121 -8.0    2.082785
... ... ... ...
1960-08-01  606 -16.0   2.782473
1960-09-01  508 -98.0   2.705864
1960-10-01  461 -47.0   2.663701
1960-11-01  390 -71.0   2.591065
1960-12-01  432 42.0    2.635484
144 rows × 3 columns

グラフに描画します。

# 対数系列をプロット
plt.plot(df['log'])

トレンドはあるものの、振れ幅(分散)はあまり変わっていないように見えます。

対数系列を取ると、分散がある程度一定に抑えられることがわかりました。

対数差分系列(Log Difference series)

階差系列への変換ではトレンドが除去でき、対数系列への変換では分散をある程度一定に抑えられることがわかったので、対数差分系列への変換を行ってみます。

対数差分系列は、時系列データの対数変換と差分の組み合わせです。具体的には、各時点の観測値に対して対数変換を適用し、その後に差分を計算します。

対数差分系列は、対数変換によって非線形性を緩和し、変動の幅を縮小させる効果があります。また、差分を取ることでトレンドや季節性の影響を除去することができます。対数変換は正規分布に近い性質を持つデータに対して効果的であり、対数差分系列は定常性の要件を満たすことが多いです。

対数差分系列の数式は次のように表されます

 \displaystyle
\Delta \log y_t = \log y_t - \log y_{t-1}

ここで、 y_t は時点  t における観測値を表し、 \log y_t はその対数変換を示します。差分系列  \Delta \log y_t は、現在の対数変換後の観測値  \log y_t から直前の対数変換後の観測値  \log y_{t-1}を引いた値として計算されます。

# 対数差分系列
df['log_difference'] = np.log10(df['Passengers']).diff(periods=1)

対数差分系列データを確認します。

# df

    Passengers  difference  log log_difference
Month               
1949-01-01  112 NaN 2.049218    NaN
1949-02-01  118 6.0 2.071882    0.022664
1949-03-01  132 14.0    2.120574    0.048692
1949-04-01  129 -3.0    2.110590    -0.009984
1949-05-01  121 -8.0    2.082785    -0.027804
... ... ... ... ...
1960-08-01  606 -16.0   2.782473    -0.011318
1960-09-01  508 -98.0   2.705864    -0.076609
1960-10-01  461 -47.0   2.663701    -0.042163
1960-11-01  390 -71.0   2.591065    -0.072636
1960-12-01  432 42.0    2.635484    0.044419
144 rows × 4 columns

グラフに描画します。

# 対数差分系列をプロット
plt.plot(df['log_difference'])

トレンドが除去でき、分散もおおよそ一定に近づいたではないでしょうか。

一方で、周期性は残っているように見えます。年単位で周期があるように見受けられるため、周期性というより季節性かもしれません。

一旦、ここまで非定常データを定常に近づけるために変換処理を行ったデータに対して、再度ディッキー・フラー検定を実施し、自己相関・偏自己相関も見てみます。

# 対数差分系列の先頭行は NaN のため除去
list = df['log_difference'][1:]

# ディッキー・フラー検定の実施
result = sm.tsa.stattools.adfuller(list)

# 結果の表示
print('ADF統計量(ADF Statistic):', result[0])
print('p値(p-value):', result[1])
print('臨界値(Critical Values):')
for key, value in result[4].items():
    print('\t%s: %.3f' % (key, value))
# 出力された結果

ADF統計量(ADF Statistic): -2.7171305983880982
p値(p-value): 0.07112054815086455
臨界値(Critical Values):
    1%: -3.483
    5%: -2.884
    10%: -2.579

ADF統計量が -2.7171305983880982 であり、p値が 0.07112054815086455 です。この場合、有意水準 5% での臨界値(-2.884)を下回っていませんが、有意水準 10% での臨界値(-2.579)を下回っています。

つまり、p 値が 0.05 より大きいため、5% の有意水準帰無仮説を棄却できませんが、10% の有意水準帰無仮説を棄却できる可能性があります。

したがって、データが非定常かどうかについてはやや曖昧な結果となりますが、有意水準 10% での臨界値を下回っていることから、データには定常性がある可能性も出てきました。変換処理に効果が出ているということですね。

自己相関と偏自己相関も見てみます。

# 自己相関をプロット
fig = sm.graphics.tsa.plot_acf(list, lags=40)

# 偏自己相関をプロット
fig = sm.graphics.tsa.plot_pacf(list, lags=40)

まだ季節性が残っていることが見受けられます。

よって完全なる定常データへ変換できたか?という問いに対しては「まだ季節性が残っているため十分ではない」とし、さらなる変換が必要ということになります。

現時点で全ての非定常性要素を除去できたわけではなりませんが、このように、通常であれば非定常な時系列データを定常データに変換していき、それを使って予測モデル(ARIMA, SARIMA etc...)を構築していく流れになります。

ARIMA モデル

本記事では割愛していますが、ARMA モデル(自己回帰移動平均モデル)というものがあります。

この ARMA モデルは、定常データに対しては説明能力は良いが非定常データには使えません。そのため、差分系列をとって定常過程に変換してから、ARMA モデルを適用することを考えます。これを ARIMA(アリマ, 自己回帰和分移動平均モデル、Autoregressive Integrated Moving Average)モデルといいます。

差分系列へ ARMA モデルを適用する場合、d 次和分過程を I(d) と書くので、真ん中に入れて “ARIMA” と呼ばれます。

ARIMA = AR 過程(Auto Regressive process, 自己回帰過程)+和分課程(I)+MA 過程(Moving Average process, 平均移動過程)

ということで、データは原系列データではなく、差分系列データを用いてモデルを作成していきます。

データの分割

データを学習用とテスト用に分けます。

今回のデータでは、1949-01 〜 1957-12-01 までのデータを学習用として使い、ARIMA モデルを構築します。

1958-01 〜 1960-12 までのデータはテスト用とし、作成したモデルで同期間の予測を行った後、このテスト用データと付け合わせて実際の予測がどれだけの精度かを確認します。

# データを学習用とテスト用に分ける
df_train_arima_diff = df['difference'][:'1957-12-01'].dropna()
df_test_arima_diff = df['difference']['1958-01-01':]

次数の決定

ARIMAモデルは、3 つの主要なパラメータ(p、d、q)によって定義されます。これらのパラメータは、モデルの自己回帰(AR)成分、積分(I)成分、および移動平均(MA)成分を制御します。

  • p(自己回帰次数)
    • 自己回帰成分の次数を示します。直前の時刻の値にどれだけの過去の値を使用するかを示します。大きな値は、過去の多くの時刻の値が予測に影響を与えることを意味します。
    • AR 過程の次数
  • d(積分次数)
    • 積分成分の次数を示します。これは、元の時系列データが非定常過程(トレンドや季節性の影響を受けて変動する)であるかどうかを示します。dの値が 0 であれば、元の時系列データは定常過程と見なされます。d の値が 1 以上であれば、差分を取ることによってデータを定常化します。
  • q(移動平均次数)
    • 移動平均成分の次数を示します。誤差項に過去の誤差値をどれだけ使用するかを示します。大きな値は、過去の誤差値が予測に影響を与えることを意味します。
    • MA 過程の次数

これらの次数を最適値を見つけるために、statsmodels の arma_order_select_ic 関数を利用します。

statsmodels.tsa.stattools.arma_order_select_ic

この関数は、情報基準(Information Criterion)を使用して異なる次数の組み合わせを比較し、最適な次数を見つけるのに役立ちます。

一般的に、AIC(Akaike Information Criterion)やBIC(Bayesian Information Criterion)などの情報基準が使用されます。

sm.tsa.arma_order_select_ic(df_train_arima_diff, ic='aic', trend='n')

結果出力

{'aic':              0           1           2
 0  1001.530812  990.101618  987.950157
 1   994.820617  987.280756  982.138924
 2   990.473898  981.180360  983.831761
 3   991.560168  983.089715  978.733996
 4   982.579395  984.165016  978.372978,
 'aic_min_order': (4, 2)}

AIC が最も低いものが最も良いモデルとされ、p=4, q=2 が AIC=978.372978 と最も低く最適な次数であるという結果が出たので、これを使っていきます。

モデル作成

ARIMA モデルは statsmodels の ARIMA() で作成できます。

statsmodels.tsa.arima.model.ARIMA

from statsmodels.tsa.arima.model import ARIMA

# ARIMA モデル作成
model = ARIMA(data_diff, order=(4, 1, 2))

result = model.fit()

推定されたパラメータを確認してみます。

# result.params

ar.L1      -0.408674
ar.L2       0.041754
ar.L3      -0.208824
ar.L4      -0.333281
ma.L1      -0.179896
ma.L2      -0.817283
sigma2    853.547001
dtype: float64

モデルの適合度と仮定の検証

作成したモデルがデータにどれだけ適合しているかを、残差を使って確認します。

ARIMAモデルは、一定の仮定に基づいています。例えば、残差は平均ゼロ、定常性を持ち、自己相関を持たないという仮定があります。残差のプロットや統計テストを通じて、これらの仮定が満たされているかどうかを確認できます。

残差の正規性を見たいのでヒストグラムをプロットします。

# 残差
res = result.resid

# ヒストグラムをプロット
plt.hist(res, bins=16)

左に寄っています。正規分布に従うとは言えないでしょう。

続いて自己相関や偏自己相関を見て、周期性ないし季節性が無いことを確認します。

fig = plt.figure(figsize=(12,8))

# 自己相関
ax1 = fig.add_subplot(211)
fig = sm.graphics.tsa.plot_acf(res.values.squeeze(), lags=40, ax=ax1)

# 偏自己相関
ax2 = fig.add_subplot(212)
fig = sm.graphics.tsa.plot_pacf(res, lags=40, ax=ax2)

周期性が残ってしまっています。

季節性成分に対応する

モデルの検証を行った結果、残念ながらこれでは予測に使えないという結論になりました。

ARIMAモデルは、データの定常性や自己相関などの統計的な特性に基づいて設計されているため、周期性が残ってしまうとモデルがデータに適合しづらくなり、予測精度が低下する可能性があります。

特に、季節性が強いデータでは、ARIMAモデルだけでは十分な適合が難しいことがあります。

サンプルデータは、前項で行った変換処理を通じて季節性のあるデータであることはわかっていましたがその上で ARIMA モデルを作成したので当然の結果ではあります。

ちなみに参考までに。

この「使えない」モデルで予測を行うと以下のようになります。青い線が正解データで、赤い線が作成したモデルを使って予測したものです。

このように、周期性が残っているデータに対しては ARIMA モデルでは限界があるため、次に季節変動ありの ARIMA である SARIMA モデルを作成していきます。

SARIMAモデル

SARIMA(Seasonal ARIMA)モデルは、ARIMA モデルに季節性成分を追加したモデルです。季節性のあるデータに適用され、季節性成分をモデル化するためのパラメータを追加します。SARIMAモデルは、季節的な変動を捉えるため、季節性パターンを特定するのに有用です。

ARIMA と SARIMA は過去の値や誤差項を用いて現在の値を予測します。ARIMA と SARIMAは一般的に季節性のないデータに適用されますが、SARIMA は季節性を持つデータにも対応できます。

データの分割

データを学習用とテスト用に分けます。分ける範囲は ARIMA のときと同じです。

  • 学習用:1949-01 〜 1957-12-01
  • テスト用:1958-01 〜 1960-12

学習用データを使って SARIMA モデルを作成後、テスト用範囲を予測、結果をテスト用データと突合させて精度を確認する。という流れです。

今回も差分系列データを使っていきます。

# データを学習用とテスト用に分ける
df_train_diff = df['difference'][:'1957-12-01'].dropna()
df_test_diff = df['difference']['1958-01-01':]

SARIMAX モジュール

SARIMAX モデルは statsmodels の SARIMAX 関数で作成できます。

statsmodels.tsa.statespace.sarimax.SARIMAX

from statsmodels.tsa.statespace.sarimax import SARIMAX

次数の決定

SARIMA モデルの次数は、元のARIMAモデルの次数(p、d、q)と季節性成分の次数(P、D、Q、s)から構成されます。

  • P(季節自己回帰次数)
    • 季節性の自己回帰成分の次数です。季節性成分のパターンをモデル化します。
  • D(季節積分次数)
    • 季節性成分の積分次数です。季節性の差分を取る回数を示します。
  • Q(季節移動平均次数)
    • 季節性の移動平均成分の次数です。誤差項に季節性の過去の誤差値をどれだけ使用するかを示します。
  • s(季節周期)
    • 季節性の周期を示します。月次データであれば 12(1年の周期)、四半期データであれば 4(1年の四半期)などです。

これらの次数を見つけるために、総当りで SARIMA モデルの次数を探索し、AIC が最も低いモデルを見つけます。

(ちなみに総当りは手段の 1 つです。データの量が多い場合や計算時間が制約されている場合には他の方法も検討できます。)

処理が多いので関数として定義します。

def find_model_with_lowest_aic(df_train):
    """
    総当りで SARIMA モデルを作成し、最も低い AIC 値を持つモデルのパラメータと AIC を出力します。

    Parameters:
    df_train (DataFrame): テスト用データ

    Returns:
    string: 最も低い AIC 値を持つモデル

    """
    
    # 各パラメータの候補値リスト
    ## p, d, q
    p_list = [1, 2]
    d_list = [1]
    q_list = [1, 2]
    
    ## 季節項(P, D, Q)
    sp_list = [1, 2]
    sd_list = [1]
    sq_list = [1, 2]

    ## 何ヶ月か(s)
    m = 12

    parameter = []
    results   = []
    
    # 総当りで SARIMA モデルを作成
    for p in p_list:
        for d in d_list:
            for q in q_list:
                for sp in sp_list:
                    for sd in sd_list:
                        for sq in sq_list:
                            # パラメータを格納
                            parameter.append([p, d, q, sp, sd, sq])
                            # モデル作成
                            model = SARIMAX(df_train, order=(p, d, q), seasonal_order=(sp, sd, sq, m))
                            aic = model.fit(disp=0).aic
                            results.append({'parmeter': [p, d, q, sp, sd, sq], 'aic': aic})
                            print('parmeter', [p, d, q, sp, sd, sq], ', AIC=', aic)

    # 比較用: 最小 AIC 値の初期値を設定
    min_aic = float('inf')
    best_result = None

    # 最小 AIC 値を持つモデルを見つける
    for result in results:
        aic = result['aic']
        if aic < min_aic:
            min_aic = aic
            best_result = result
    
    print("最も低い AIC 値を持つモデル:", best_result)

各パラメータの候補値リスト(p_list, d_list, q_list, sp_list, sd_list, sq_list`)に定義されている数字は、それぞれ SARIMA モデルの次数に対する候補値です。これらの値を組み合わせてモデルを構築し、AIC を計算して最適なモデルの次数を決定します。

具体的な値はデータやドメイン知識に基づいて選択する必要がある。として、決め打ちです。とはいえ過学習のリスクや計算量コストを考えると、基本的には以下に沿うことになるかなと思っています。

  • p_list(自己回帰次数の候補値)
    • SARIMA モデルの自己回帰次数(AR次数)に関する候補値を指定
    • 通常、小さな整数値(1から3程度)を選択
  • d_list(積分次数の候補値)
    • SARIMA モデルの積分次数(差分の取る回数)に関する候補値を指定
    • データの定常性に応じて、0 または 1 を選択
  • q_list(移動平均次数の候補値)
    • SARIMA モデルの移動平均次数(MA次数)に関する候補値を指定
    • 通常、小さな整数値(1から3程度)を選択
  • sp_list(季節自己回帰次数の候補値)
    • 季節性の自己回帰次数(季節AR次数)に関する候補値を指定
    • 通常、小さな整数値(1から3程度)を選択
  • sd_list(季節積分次数の候補値)
    • 季節性の積分次数(季節差分の取る回数)に関する候補値を指定
    • データの季節性に応じて、0 または 1 を選択
  • sq_list(季節移動平均次数の候補値)
    • 季節性の移動平均次数(季節MA次数)に関する候補値を指定
    • 通常、小さな整数値(1から3程度)を選択

それではこの関数を使って次数を決定していきます。

# warning 多ければ抑制
# import warnings
# warnings.filterwarnings('ignore')

# 関数実行
find_model_with_lowest_aic(df_train_diff)

出力された結果は以下

parmeter [1, 1, 1, 1, 1, 1] , AIC= 706.6661313300135
parmeter [1, 1, 1, 1, 1, 2] , AIC= 700.7887100270527
parmeter [1, 1, 1, 2, 1, 1] , AIC= 702.2413978900132
parmeter [1, 1, 1, 2, 1, 2] , AIC= 701.9189059594827
parmeter [1, 1, 2, 1, 1, 1] , AIC= 707.7131926442661
parmeter [1, 1, 2, 1, 1, 2] , AIC= 702.5540894596875
parmeter [1, 1, 2, 2, 1, 1] , AIC= 703.8802139331958
parmeter [1, 1, 2, 2, 1, 2] , AIC= 703.5023730470815
parmeter [2, 1, 1, 1, 1, 1] , AIC= 708.2181279257733
parmeter [2, 1, 1, 1, 1, 2] , AIC= 702.7085048503061
parmeter [2, 1, 1, 2, 1, 1] , AIC= 704.100556663309
parmeter [2, 1, 1, 2, 1, 2] , AIC= 703.745258100657
parmeter [2, 1, 2, 1, 1, 1] , AIC= 709.7110131616942
parmeter [2, 1, 2, 1, 1, 2] , AIC= 700.8798954969151
parmeter [2, 1, 2, 2, 1, 1] , AIC= 702.3974872249754
parmeter [2, 1, 2, 2, 1, 2] , AIC= 702.2549954185089

最も低い AIC 値を持つモデル: {'parameter': [1, 1, 1, 1, 1, 2], 'aic': 700.7887100270527}

最も低いAIC値を持つモデルは、パラメータ p=1, d=1, q=1, P=1, D=1, Q=2 のものとなりました。 これらを当てはめて再度モデルを作成します。

# SARIMA モデル作成
#        SARIMAX(df_train_diff, order=(p,d,q), seasonal_order=(P,D,Q,m)).fit()
r_diff = SARIMAX(df_train_diff, order=(1,1,1), seasonal_order=(1,1,2,12)).fit()
# r_diff.summary()

SARIMAX Results

Dep. Variable:  difference  No. Observations:   107
Model:  SARIMAX(1, 1, 1)x(1, 1, [1, 2], 12) Log Likelihood  -347.466
Date:   Fri, 18 Aug 2023    AIC 706.931
Time:   08:31:16    BIC 722.191
Sample: 02-01-1949  HQIC    713.095
      - 12-01-1957      
Covariance Type:    opg     

          coef      std err z   P>|z|    [0.025  0.975]
ar.L1     -0.2249   0.087   -2.599  0.009   -0.395  -0.055
ma.L1     -0.9964   0.427   -2.332  0.020   -1.834  -0.159
ar.S.L12  0.5576    0.727   0.768   0.443   -0.866  1.982
ma.S.L12  -0.8143   0.772   -1.055  0.291   -2.327  0.698
ma.S.L24  0.2828    0.213   1.330   0.184   -0.134  0.700
sigma2  77.6646 32.494  2.390   0.017   13.977  141.352

Ljung-Box (L1) (Q): 0.04    
Jarque-Bera (JB):   3.46
Prob(Q):    0.85    
Prob(JB):   0.18
Heteroskedasticity (H): 1.36    
Skew:   0.44
Prob(H) (two-sided):    0.40    
Kurtosis:   2.69

モデルの適合度と仮定の検証

作成したモデルがデータにどれだけ適合しているかを、残差を使って確認します。

ARIMA のときはヒストグラムやコレログラムなどを個別に見ていきましたが、statsmodels の plot_diagnostics 関数を使うと複数の残差の診断プロットを一回で生成できます。

statsmodels.tsa.arima.model.ARIMAResults.plot_diagnostics

# 残差の診断プロット
r_diff.plot_diagnostics(lags=20);

plot_diagnostics 関数は、ARIMA および SARIMA モデルの残差の診断を行うために使用されます。生成される 4つのチャートは、モデルの残差が一定の基準を満たしているかどうかを確認するためのものです。

Standardized Residuals Plot (標準化残差プロット)
このプロットは、モデルの残差を標準化したものを時系列で表示します。残差がランダムにばらついていることが期待されます。もし残差にパターンやトレンドが見られる場合、モデルがデータに適合していない可能性があります。
Histogram Plus Estimated Density Plot (ヒストグラムと推定密度プロット)
このプロットは、残差のヒストグラムカーネル密度推定を表示します。残差が正規分布に近いかどうかを確認するのに役立ちます。理想的には、ヒストグラムと密度推定の形状が正規分布に近い形になることが望ましいです。
Normal Q-Q (Quantile-Quantile) Plot (正規Q-Qプロット)
このプロットは、残差の分位数を正規分布の分位数と比較したものです。正規分布に従う場合、プロットされた点は対角線に近い位置に分布します。プロットされた点が対角線から外れている場合、残差が正規分布から逸脱している可能性があります。
Correlogram (自己相関プロット)
このプロットは、残差の自己相関係数をタイムラグに対してプロットします。残差が白色雑音(無相関性)である場合、自己相関プロットはほとんどのラグでゼロに近くなるはずです。自己相関プロットにおけるラグがゼロでない値を示す場合、残差に時系列的なパターンが残っている可能性があります。

パターンやトレンドはなく、まあまあ正規分布に近く、QQ プロットはおおむね対角線に乗っている。コレログラムでも、周期性・季節性は見られない。ということで、おおそよ良いのではないかと思われます。

では、作成したモデルを使って実際に予測を行い、テスト用データと突合させてみます。

pred_diff = r_diff.predict('1958-01-01', '1960-12-01')

予測結果

1958-01-01    10.862560
1958-02-01   -12.957117
1958-03-01    51.532015
1958-04-01    -6.048657
1958-05-01     6.919694
1958-06-01    68.024899
1958-07-01    44.618030
1958-08-01    -2.338867
1958-09-01   -60.685910
1958-10-01   -57.188398
1958-11-01   -41.271487
1958-12-01    34.165328
1959-01-01    10.742535
1959-02-01   -13.216177
1959-03-01    53.727288
1959-04-01    -6.715341
1959-05-01     7.652743
1959-06-01    71.907824
1959-07-01    44.975607
1959-08-01    -0.446448
1959-09-01   -63.921007
1959-10-01   -59.853134
1959-11-01   -42.304585
1959-12-01    33.934304
1960-01-01    11.246330
1960-02-01   -13.367209
1960-03-01    55.074747
1960-04-01    -6.993050
1960-05-01     8.162161
1960-06-01    74.172291
1960-07-01    45.274499
1960-08-01     0.708273
1960-09-01   -65.625626
1960-10-01   -61.239695
1960-11-01   -42.781264
1960-12-01    33.904902

ここまで差分系列データで進めてきたため、結果も差分系列データのままです。実際の乗客数データに変換して戻します。

def inverse_difference(initial_value, diff_series):
    """
    差分系列から原系列に戻す

    Parameters:
    initial_value (int): 予測開始した月の前月の乗客数
    diff_series (int): 差分系列データ

    Returns:
    diff_series: 原系列データと同じ次元の予測データ

    """
    cum_sum = diff_series.cumsum()
    original_series = cum_sum + initial_value
    return original_series
# 1957-12-01 の乗客数
initial_value = df['Passengers']['1957-12-01']
# 差分系列から原系列に戻す
pred_original = inverse_difference(initial_value, pred_diff)

変換結果

1958-01-01    346.862560
1958-02-01    333.905443
1958-03-01    385.437459
1958-04-01    379.388802
1958-05-01    386.308495
1958-06-01    454.333395
1958-07-01    498.951424
1958-08-01    496.612558
1958-09-01    435.926648
1958-10-01    378.738250
1958-11-01    337.466764
1958-12-01    371.632091
1959-01-01    382.374626
1959-02-01    369.158449
1959-03-01    422.885737
1959-04-01    416.170397
1959-05-01    423.823140
1959-06-01    495.730964
1959-07-01    540.706571
1959-08-01    540.260123
1959-09-01    476.339117
1959-10-01    416.485983
1959-11-01    374.181398
1959-12-01    408.115702
1960-01-01    419.362032
1960-02-01    405.994823
1960-03-01    461.069570
1960-04-01    454.076520
1960-05-01    462.238681
1960-06-01    536.410972
1960-07-01    581.685471
1960-08-01    582.393744
1960-09-01    516.768118
1960-10-01    455.528423
1960-11-01    412.747158
1960-12-01    446.652061

予測した結果をプロットしてみます。

# 正解データと予測結果をプロット
plt.plot(df['Passengers'])
plt.plot(pred_original, "r")

青い線が正解データ。赤い線が実際に予測した部分です。

多少上ぶれている部分はあるものの、そこそこトレンドや季節性を掴めていることが確認できます。

精度検証

予測が出来たので、精度を検証してみます。

時系列データに使える精度指標としては以下になります。

  • RMSE
  • MAPE

RMSE(Root Mean Square Error, 二乗平均平方根誤差)

RMSE は、予測モデルの予測と実際の観測値との間での誤差を評価するための指標です。RMSE が小さいほど予測が実際のデータに近いことを示します。

 \displaystyle
RMSE = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2}

 n はデータポイントの数、 y_i は実際の値、 \hat{y}_i は予測値です。

scikit-learn の mean_squared_error 関数を利用します。

sklearn.metrics.mean_squared_error

from sklearn.metrics import mean_squared_error

test_original = df['Passengers']['1958-01-01':]

np.sqrt(mean_squared_error(test_original, pred_original))

# => RMSE: 21.32544528858937

RMSE は 21.3 でした。これは、予測値と実際の値との平均誤差がおおよそ 21.3 であることを意味します。

300 〜 600 くらいの予測なのでこの平均誤差が多いか少ないかは要件によるところもありますが、まだ改善はできそうです。これが一桁の平均誤差ならかなり精度の高い予測ができていると言えそうです。

MAPE(Mean Absolute Percentage Error, 平均絶対パーセント誤差)

MAPE は、予測モデルの予測値と実際の観測値との間での誤差を評価するための指標です。予測の正確さをパーセントで示します。MAPEの値が小さいほど予測が正確であることを示し、大きいほど予測の誤差が大きいことを示します。

 \displaystyle
MAPE = \frac{1}{n} \sum_{i=1}^{n} \left| \frac{y_i - \hat{y}_i}{y_i} \right| \times 100

 n はデータポイントの数、 y_i は実際の値、 \hat{y}_i は予測値です。

np.mean(np.abs((pred_original - test_original) / test_original))

# => MAPE: 0.045705066956362256

MAPE 0.0457 でした。これは予測モデルの予測が実際の観測値と比べて、平均して約 4.57 %の誤差があることを示しています。つまり、予測値が実際の値から約 4.57 %程度離れていることが平均的な誤差として示されています。

値としては小さいため、モデルの予測が相対的に実際の観測値に近いと判断できそうです。

モデルの改善

ここまでで、「データの確認・理解」「定常データへの変換」「モデル構築」と一連の時系列分析の流れを実施しました。

あとは作成したモデルの精度を上げてく工程がありますが、本記事ではここまでとします。

「モデルのチューニング・パラメータ調整」「データの変換」など、アプローチは色々とあるため、次の機会に実施したいと思います。

トレンドや季節性成分の抽出

最後に、原系列データからトレンドや季節性成分を抽出してみます。

python の Pmdarima というパッケージを利用します。

alkaline-ml/pmdarima

Pmdarima(Pyramid ARIMA)は、Pythonで時系列データの予測モデリングを行うためのツールキットです。 ARIMA, SARIMA モデルのパラメーター推定と予測モデリングのプロセスを簡素化し、ユーザーが容易に時系列データを分析できるように支援してくれます。

Pmdarima パッケージを利用すると、モデル作成のためのパラメータ(次数)を自動で選択してくれたり、モデルの適合度を評価・診断プロットを生成する機能があったりと、今回実施した手順を簡略化してモデル構築が行えたりします。

Pmdarima パッケージを利用したモデル構築は次の機会に試すとして、ここではトレンドや季節性成分の抽出を行ってみます。

# データセット読み込み
column_names = ['Month', 'Passengers']
df=pd.read_csv('AirPassengers.csv', index_col='Month', parse_dates=True, names=column_names, header=None, skiprows=1)
# df.head()
Month   Passengers
1949-01-01  112
1949-02-01  118
1949-03-01  132
1949-04-01  129
1949-05-01  121

この原系列データからトレンドや季節性成分を抽出します。

decompose 関数で成分の分解を行い、decomposed_plot 関数でそれぞれの結果をプロットします。

from pmdarima import utils
from pmdarima import arima

data = df['Passengers'].values

utils.decomposed_plot(
    arima.decompose(data,'additive',m=12), figure_kwargs = {'figsize': (16, 12)} 
)

  • data
    • 元のデータ
  • trend
    • トレンド
  • seasonal
    • 季節性成分
  • random
    • 残差成分
    • 残差成分はトレンドと季節性を除いた残りの部分を表現しています。通常、残差はランダムであるべきで、特定のパターンや構造がないことが望まれます。

トレンドと季節性成分を抽出できました。

Pmdarima パッケージも便利そうなので次の機会に深掘ってみたいと思います。


現在 back check 開発チームでは一緒に働く仲間を募集中です。 herp.careers

エンドユーザー向けプロダクトの構築とマイクロサービス化

この記事は個人ブログと同じ内容です

エンドユーザー向けプロダクトの構築とマイクロサービス化

こんにちは、株式会社ROXX で back check とうサービスを開発しているぐっきーです。 今回は back check で新しく toC 向けのプロダクトを新規リポジトリとして構築したので、その概要を紹介します。 なおこの記事では新規コードベースの立ち上げ、アーキテクチャについて説明しますが、サービスリリース自体はまだであるため、あくまで現状の進捗共有ということで解説していきす。

アーキテクチャの選択と背景

マイクロサービス

モノリスのコードベースで運用していたときの問題点として、既存のコードベースに負債もありつつ、プロセスの中でボトルネックとして話題に上がっていた問題として同じコードベースを複数チームで開発することに対するチーム間のコミュニケーションコストがありました。 これらを考慮して新規のプロダクトを立ち上げるタイミングで、責務の違う領域として新しくリポジトリを立てることとしました。 また開発者目線でも新しい技術やアーキテクチャを取り入れる機会ということで興味があったので純粋な興味という部分もありました。実際に現時点でできあがったプロダクトではメンバーの得意な技術やレイヤーを整理したアーキテクチャを取り入れたことで総合的に開発者体験は上がったと感じています。

技術スタック

技術選定

DynamoDB

DynamoDB を採用した意図としては、主に以下になります。 - RDS の料金と比較してデータの読み書きの量に応じた価格設定のため、 back check のサービスの性質上、大量アクセスを捌くようなビジネスドメインではないため値段が安く抑えられる。 - RDS だと MySQL のバージョンアップなどに伴うダウンタイムやメンテナンスコストが発生するが Dynamo DB ではそれが不要になる。 - Dynamo DB で Single Table Design を採用すると join によるテーブルを跨いだデータアクセスが不要になり早いらしい。

そもそものきっかけとしては Tech Lead が Dynamo DB の採用を提案してくれましたが、チーム自体に知見はない状態だったので不安はありました。しかし back check の開発組織としてもキャッチアップにコストをかけることが許容してもらっていたため学習を前提としつつも採用することができました。

BFF

プロダクト自体が toC 向けということもあり、 back check 上で行うリファレンスチェックの候補者、推薦者フローを今後移植してくることを想定していたため、認証サービスを共通で使えるようにするということを一番の目的として BFF を採用しました。 また front と bff 間の通信を GraphQL を採用し、スキーマから型生成させることで型安全にアクセスできるようにしました。

Lambda

BFF, 各種 backend はそれぞれ個別のサービスとして lambda 上で動かしています。

Lambda で動かすことで各サービスの実行環境の運用をマネージドサービスに委譲しつつ、Dynamo DB ストリームによるイベント処理や、SQS, SNS を使ったキューイング、メッセージングをフックに連鎖的に処理を実行させることでサービス間の連携を行っています。

共通の DI コンテナ

各サービスの初期化時に DI コンテナを一通り初期化させ読み込むような実装となっています。 サービス毎に実行環境は分かれつつ、モジュラーモノリスのようにモノリポ全体のレイヤーを domain, repository(DynamoDB, backcheck_api など個別に用意している) とまとめて?管理しており、それぞれの bind を共通の DI コンテナを使って行っているため、全体の構造把握がしやすいのがメリットです。

※ ちなみにコードベース内のレイヤーの設計は厳密に DDD を採用しているわけではありません。どちらかというと DDD の設計パターンを参考にしているといった温度感で設計しています。

また全体的に抽象(インターフェース)に依存させる設計となっているため、依存関係逆転の法則でよく言われるテストのしやすさや、再利用性の向上もありつつ、テスト駆動開発のようにスコープを絞った開発ができることから、小さいスコープで着実に開発できることも嬉しいポイントです。

Dynamo DB の設計

Dynamo DB の特性を活かせるようにという意図で、Single Table Design を採用しました。 Single Table Design では join を使ってリレーションを表現するようなことができないため、設計方法のキャッチアップに苦労しました。

私たちがとった設計の流れとしては、まず RDS のように ER 図を起こしやりたいことを可視化し、そこから管理したい各データへのアクセスパターンを洗い出します。アクセスパターンを元にどのデータをまとめて持たせるとよいかを設計し、Table の Entity を起こしていくといった方法で進めていきました。

このとき RDB の考え方と大きく違う部分として、Single Table Design では参照したいデータをマスターデータからコピーして Entity の Item に格納するように設計します。こうすることによって、アクセスパターン毎に join してテーブルを跨いだデータアクセスをする必要がなく、データ取得までの速度が速くなります。

実際に設計してみた所感として、設計の考え方の違いからとっつきづらさはあったものの、気軽に参照用のデータを捨てられる点など RDB の基本的な設計では得られないメリットにより、変更がしやすくなったように思います。

Dynamo DB の設計について詳しく知りたい方は「The DynamoDB Book」という書籍が実例を添えて詳しく説明してくれているのでおすすめです。

サービス間連携

データ連係の部分は SQS を用いたキューイングをトリガーに Lambda を起動し、 backcheck_api で内部的に公開しているエンドポイントに直接 fetch する方法で実装しています。(今回のケースでは backcheck_api でマスターとなるデータを持っており、新プロダクト側で複製したデータを保存し、加工して View で表示させています)

サービスが独立して稼働できるように担保するための設計を意識しましたが、データ連携周りは DeadLetterQueue から復帰させるケースを考慮したりと単体のアプリケーション内では考える機会の少なかった部分まで考慮する必要があり、設計に苦労しました。

また、チーム間のデータ連携が必要な部分に関して、大きな部分は backcheck_api 側を管理するチームのリーダーと弊チームのリーダーで調整を行ってもらうことで解決しました。背景として、 backcheck_api 側の実装タスクを起こして依頼するフローとしていたのですが、オーバーオールリファインメントの場などでチケットの説明が必要であり、この場に毎回出席してくれているチームリーダーに調整役となってもらうこととしました。

チーム間の連携が必要な部分に関してはこれからもでてくると思いますが、今後の展開として調整作業のバス係数が2人以上になるような属人化を省く仕組みを考えていけたらいいなと個人的には思っています。

テスト設計

テスト設計についてはそれぞれユニットテストで担保しつつ、まだアプリケーション全体を通したテスト設計までは詰められておりません。 現状はユニットテストで補えない箇所は手作業による統合テストとモンキーテストによって行っています。 今後各サービス間の連携部分の結合を網羅するテストを全部手動で運用していくことはつらいため、この辺は E2E テストを採用する話が上がっている状況であり、技術選定中です。

front については Jest と testing-library/react を用いて各 Hooks と UI などのテストを実装しています。 また UI が定まってきたら Chromatic によるビジュアルリグレッションテストを導入予定です。

ログ戦略

ロギングの詳細な設計についてはまだ追いついていない状況です。 現状はサービス間の受け渡しなど、処理のつなぎ目となるところでログを仕込んでおり、datadog に流して管理しています。

余談ですが、AWSXRay によって、処理がどこまで到達したか。各セクションでどの程度実行速度が掛かったか。が可視化されているため非常に便利です。

おわりに

さて、以上が大まかな back check の toC 向けプロダクトの概要説明でした。まだまだ一般公開しておらず、よく言われるマイクロサービスの運用においてのつらみについてはまだ充分に学習できていませんが、今後も柔軟に対応しつつ開発組織として知見を溜めてどんどん展開していけたらと思っております。 また、今回の内容に含められなかった部分についても今後どしどし紹介していきたいと思います。 最期になりますが、back check ではモダンなアーキテクチャや、組織開発、HR Tech 領域に興味のある方を絶賛募集しております。もし上記の内容にご興味を持っていただけたら、お気軽にカジュアル面談ご依頼ください。

back check のカジュアル面談の窓口が見つからなかったのでお隣の agent bank 事業部の求人を貼っておきますw

herp.careers

また個人的に話を聞いてみたいなども大歓迎ですので、お気軽に DM いただければと思います。

twitter.com