default eye-catch image.

ロジスティック回帰

[mathjax] 参考書籍を読んでいく. 今回はロジスティック回帰. 未知のサンプルのクラスの所属確率を見積もることができる, という重要なことが書かれている. また重みの学習は勾配降下法のフレームワークの中で統一されていて一連の理解が進む. 一度は理解しておいた方が良い内容だと思った. パーセプトロンとADALINE 線形和(z=w^T)を考えたとき 「サンプル(x)が決定境界の上にある」 というのは(w^Tx=0). (w^T x > 0), (w^T x < 0) を決定境界の 「こちら側」 か 「向こう側」 か対応させる. パーセプトロンのアイデアは以下の通り. 線形和 (z=w^T x)を決定境界とし, (phi(z)in (-1,+1))を予測ラベルとする 予測が誤った回数, つまり真のラベルと予測ラベルの差の合計がゼロになるように(w)を学習する (phi(z))はステップ関数で連続ではないので解析的な式の変形ができない 決定境界により完全に分離できなければ学習が収束することはない 決定境界から離れている度合いを連続で凸な関数として定義し, その関数が極小となる(w)を求めていこう, というアイデアに拡張したのがADALINE. (phi(z) = z). コスト関数(J(w) = frac{1}{2} sum_i left( hat{y^{(i)} - phi ( z^{(i)})} right)^2 ) トレーニングセット全体から(J(w))の極小化を考えることが可能. (勾配降下法) トレーニングセットの一部を使って(J(w))の極小化を考え, 他のトレーニングセットで反復することも可能. (確率的勾配降下法) ロジスティック回帰 決定境界を表す線形和(z=w^T x)について(phi(z) in (mathbb{R}| 0 leq z leq +1))を考える. 線形和(z)がクラス1に分類される確率を表す(phi(z))を考える. (phi(z))をどう作るのか 線形和を(0)から(1)の実数全体に変換する関数を考える. オッズ比の対数をとったロジット関数(logit(p))を使う. ここで (p)は(x)が与えられたときにサンプルデータがクラス1に属する確率. begin{eqnarray} logit(p(y=1 | x)) &=& log frac{p}{1-p} \\ &=& w_0 x_0 + w_w x_w + cdots + w_m x_m \\ &=& sum_{i=0}^m w_i x_i \\ &=& w^T x end{eqnarray} つまり, 線形和(z = w^T x in (mathbb{R} | 0 leq z leq 1) ) (p)は未知であるということが重要. この式から(p)を予測していく. (f(p) = log frac{p}{1-p}) の逆関数が分かれば良い. begin{eqnarray} f(p) &=& log frac{p}{1-p} \\ e^{f(p)} &=& frac{p}{1-p} \\ (1-p)e^{f(p)} &=& p \\ e^{f(p)} &=& p(1+e^{f(p)}) \\ p &=& frac{e^{f(p)}}{1+e^{f(p)}} \\ p &=& frac{1}{1+e^{-f(p)}} end{eqnarray} (f(p)=z)としていたので以下のようになる. (zとpが混在していてたぶん数式としての表現は間違ってる.. ) begin{eqnarray} p &=& frac{1}{1+e^{-z}} end{eqnarray} (p)は確率なので(p in (mathbb{R}|0 leq p leq 1)). (phi(z) in (mathbb{R}|0 leq p leq 1) )とすると, begin{eqnarray} phi(z) = frac{1}{1+e^{-z}} end{eqnarray} (z-phi(z))をGeoGebraでプロットすると以下のような感じ. (z)を大きくしていくと(phi(z))は限りなく(1)に近づく. (z)を小さくしていくと(phi(z))は限りなく(0)に近づく. (phi(z)=)の出力は正事象が起こる確率であると解釈される. (phi(z)=0.8)である場合, 80%の確率で正事象が起こり,20%の確率で正事象が起こらないことを表す. トレーニングデータからパラメタを学習し, ある(phi(z))が得られたとして, 未知のサンプルのクラスの所属関係を確率で見積もることができる. 天気予報で晴れの確率70%、晴れでない確率30%のように. 予測の際に,クラスの所属関係の確率を見積もることができるのが神がかるレベルで重要!! もし(phi(z))を2値分類に当てはめるのであれば, (phi(z)=0.5)を境に上半分を(1)、下半分を(0)と予測するものとする. begin{eqnarray} hat{y} = begin{cases} 1 & phi(z) ge 0.5 \\ 0 & phi(z) lt 0.5 end{cases} end{eqnarray} それって, (z=0)を境に左半分, 右半分に割り付けるのと同じなので, begin{eqnarray} hat{y} = begin{cases} 1 & z ge 0 \\ 0 & z lt 0 end{cases} end{eqnarray} ロジスティック回帰の重みの学習 では(w)をどうやって学習するか. 式をイジるだけで面倒なだけで,これ以上は洞察得られないなと思うので, 流れだけ書いてみる. 学生時代に形態素解析っぽい何かのアルゴリズムを考えたことがあって, 形態素どうしのベターな接続を求め際に, 接続コストをコーパスから作った確率で表して, 尤度を最大化する問題として解いた記憶があって, 今更合点が行くという謎. 勉強不足で尤度最大化のアイデアがあってそうした訳ではないので, もっと勉強していたらよかったな..と. ADALINEの説明の中で, 真の値と予測値の差の2乗誤差をコスト関数として, 連続で凸な関数の極小化の問題として(w)を求めていった. ロジスティック回帰の場合, 予測は(phi(z))も連続で凸な関数なので, 同じロジックで(w)を求めることもできる. コスト関数(J(w))は同様に以下. begin{eqnarray} J(w) &=& sum_i frac{1}{2} left( phi(z^{(i)}) - y^{(i)} right)^2 end{eqnarray} (phi(z)=frac{1}{1+e^{-z}})だと, これを解析的に微分するのは不可能. トレーニングデータが(m)個あった場合に(i)番目のトレーニングデータにおける確率は以下. (x)も(w)もパラメタですよ,って書き方. begin{eqnarray} P(y|x;w)=P(y^{(i)}|x^{(i)};w) end{eqnarray} 求めたいのは, 全てのトレーニングデータについて(P(y))の積が最大になる(w). 積,つまり尤度を書き下すと以下のようになる. begin{eqnarray} L(w) &=& prod_{i=1}^m left( P(y^{(i)}) | x^{(i)};w right) \\ &=& prod_{i=1}^m left( phi(z^{(i)}) right)^{y^{(i)}} + left( 1-y^{(i)}right) log left( 1-phi(z^{(i)}) right) end{eqnarray} 尤度そのものの最大化するよりも対数をとったものを最大化する方が楽になる. (対数をとると指数の肩が定数倍になり積が和になる. 相当簡単になる. ) begin{eqnarray} l(w) &=& log L(w) \\ &=& sum_{i=1}^{n} left( y^{(i)} log( phi(z^{(i)}) ) + (1-y^{(i)}) log (1-phi(z^{(i)})) right) end{eqnarray} これを最大化するということは, これに(-1)を掛けたものを最小化するということ. そもそもコスト関数(J(w))を立てて最小化しようとしていたので, これを(J(w))と置いてしまう. begin{eqnarray} J(w) = sum_{i=1}^{n} left( -y^{(i)} log( phi(z^{(i)}) ) - (1-y^{(i)}) log (1-phi(z^{(i)})) right) end{eqnarray} ADALINEの勾配降下法で, コスト関数を最小にするように(w)を更新していった. つまり, (w := w - Delta w)という更新をしていった. ロジスティック回帰において, コスト関数を対数尤度で書き直していて, 対数尤度を最大化するように(w)を更新する. つまり, (w := w + Delta w)という更新をおこなう. 対数尤度を最大化する(w)を更新する話は, コスト関数を最小化する(w)を更新する話と同じ, なので, 結局のところ勾配降下法と同じ式となる. begin{eqnarray} Delta w_j &:=& - eta frac{partial J}{partial w_j} end{eqnarray} 尤度関数の偏導関数はシグモイド関数の偏導関数を使って以下のようになるらしい. (省略) シグモイド関数の偏導関数は以下 begin{eqnarray} frac{partial phi(z)}{partial z} &=& frac{1}{partial z} frac{1}{1-e^{-z}} \\ &=& frac{1}{1+e^{-1}} left( 1- frac{1}{1+e^{-z}} right) \\ &=& phi(z) (1-phi(z)) end{eqnarray} 対数尤度の偏導関数はシグモイド関数の偏導関数を使って以下. begin{eqnarray} frac{partial l(w)}{partial w_j} &=& left( y frac{1}{phi(z)} - (1-y) frac{1}{1-phi(z)} right) frac{partial}{partial w_j} phi(z) \\ &=& left( yfrac{1}{phi(z)} - (1-y) frac{1}{1-phi(z)} right) phi(z)(1-phi(z)) frac{partial}{partial w_j} z \\ &=& left( y(1-phi(z))-(1-y)phi(z) right) x_j \\ &=& left( y-phi(z) right) x_i end{eqnarray} コスト関数の最小化と対数尤度関数の最大化が同じ,というロジックが効いて, ADALINEの更新と全く同じになる. begin{eqnarray} w_j &:=& - eta frac{partial J}{partial w_j} \\ &=& eta sum_{i=1}^n left( y^{(i)} -phi(z^{(i)}) right) x_j^{(i)} end{eqnarray} 奇跡的..

default eye-catch image.

機械学習の分類問題と損失関数の最小化の話

[mathjax] 参考にした書籍でこの順序で誘導されていて理解しやすかったです. パーセプトロンによる一番簡単な教師あり学習を理解する ADALINEにより学習を凸で連続なコスト関数の最小化問題として捉える パーセプトロンの学習 2値のラベル((+1),(-1))が付与されているサンプルデータが与えられたとする. ラベル値が(-1)であるデータと, ラベル値が(+1)であるデータに分類できる. それらの境界が線形和で表される問題である場合, その境界を求めることができれば, 未知のデータに対してその境界の(+1)側か(-1)側かを判定できる. (m)次のサンプルデータと重みの線形和を考える. [ boldsymbol{z} = boldsymbol{w}^T boldsymbol{x} = w_1 x_1 + w_2 x_2 + cdots x_m x_m ] (z)に関するステップ関数を用意する. こうすることで (z)-(phi(z)) 空間において(z=theta)を境に線形和(z)が(-1),(+1)に分かれることを表現できる. [ varphi(z) = begin{cases} 1 & (z gt theta) \\ -1 & (z leq theta) end{cases} ] (theta)を左辺に移動し, (x_0 = 1), (w_0 = -theta) とすると, 線形和に(theta)の項を組み入れることができる. 新しい線形和(z\')が(0)になる箇所を境にステップ関数の応答値が切り替わる. [ varphi(z) = begin{cases} 1 & (z-theta gt 0) \\ -1 & (z-theta leq 0) end{cases} ] [ z\' = z - theta = w_0 x_0 + w_1 x_1 + w_2 x_2 + cdots x_m x_m ] あるパラメータ(boldsymbol{w})が存在するとして, データ(boldsymbol{x})について, (boldsymbol{w}^T boldsymbol{x}=0)を境界にステップ関数の応答値が切り替わる. 未知のデータ(x)に対して, (boldsymbol{w}^T boldsymbol{x})はラベルの予測値(+1), (-1)を応答する. 予測値(varphi(w^Tx))が正解であるサンプルデータに含まれる(y^{i})と同じとなる(w)を探したい. それを探していく. 最初,(w)に初期値を設定し, 以下の手続きによって(w)を更新していく. 上付きの添字はサンプルデータ内の順序数を表している. (y^{(i)})は(i)番目のサンプルデータ. 右下の添字は該当サンプルデータの各次元を表している. (y^{(i)_m})は(y^{(i)})の(m)番目の要素. 現在の(w)を使って予測値を計算する. (hat{y}=w^T x) (w)を更新する. (Delta w_j := eta(y^{i}-hat{y})x^{i}_j) 1個のサンプルデータで1回(w)が更新される. そもそもデータセットを綺麗に線形和が表す決定境界で分離できないような状態だと、 何度やっても予測したクラスラベルと真のクラスラベルの差が埋まらない。 あっちを立てればこっちが立たず、みたいになる。 予測したクラスラベルと真のクラスラベルを比較するこの方法だと、 (Delta w_j := eta(y^{i}-hat{y})x^{i}_j) が良い値なのか悪い値なのか、繰り返さないとわからない。 予測と真の値の比較を凸で連続な関数で表すことができれば、その関数の凸の底に向かうやり方で、 「今より良い次の値」に更新する方法を解析的に求めることができて都合が良い。 真の値と予測した値の差が「損失」とか「コスト」とか呼んで、 「コスト関数」を最小化するパラメータが良いパラメータなんだろう、という話が進んでいく。 ADALINEの学習 線形和をステップ関数に変換する部分があった。 こうして(w^T x varphi(w^T x))空間で (w^T x)が(-1),(+1)に分かれる(w)を求めようとした. [ varphi(z) = begin{cases} 1 & (z-theta gt 0) \\ -1 & (z-theta leq 0) end{cases} ] そうではなく以下の恒等式(左辺と右辺が同じ)を使い,真の値と予測した値の差の求め方を変えると, 差が凸で連続な関数で表せるようになり (解析的に微分できるようになり), 一番小さいところは? という話がしやすくなる. [ varphi(z)=z ] 最小二乗法のときやったやつで、差の2乗を足し合わると符号がキャンセルされてよくて、差を式で表せる。 [ J(w) = sum_i left( y^{(i)} - varphi(w^T x^{(i)}) right)^2 ] こういうのを誤差平方和と言うようで(frac{1}{2})倍するらしい. [ J(w) =frac{1}{2} sum_i left( y^{(i)} - varphi(w^T x^{(i)}) right)^2 ] (J(w))がなんで連続で凸なのかは知ったことではないが、 連続で凸であれば(J\'(w)=0)を解くと極小となる(w)が求まるのは高校生のときにやった話. (J(w))の接線(多次元だと接っする面)の傾きが(J\'(w))。 ただ(w)には次数があって傾きは以下(それぞれの次数毎の極限). [ nabla J(w) = frac{partial J(w)}{partial w_j} ] (w J(w))空間で(nabla J(w))は(w)における傾きなので, (w)に(-nabla J(w) cdot 1)を足すと, (J(w))が極小になる箇所に近づく. どれだけ行けば良いかわからないので(-nabla J(w) cdot eta)を足すことにする. (w)を以下の通り更新する. begin{eqnarray} w &:=& w + nabla J(w) \\ &:=& w -nabla J(w) cdot eta end{eqnarray} ちなみに(nabla J(w))は式をこねくり回すと計算できる. 最終的に更新は以下の通りとなる. [ w := w - eta sum_i left( y^{(i)} -varphi left( z^{(i)} right) right) x_j^{(i)} ] パーセプトロンの更新とそっくりな形が出てきて感動する... あっちは(varphi(z))が不連続なステップ関数でこっちは連続な関数. 式をこねくり回す際に, (varphi(z) = z)の恒等式の関係を使っていない. 何か連続な関数であればこれが成り立つ. 形式上,例えば(varphi(z))を以下(シグモイド)とすることでロジスティック回帰 (本当か? 次回確認.). [ varphi (z) = frac{1}{1+e^{-z}} ]

default eye-catch image.

Grafanaプラグインを読んでいく – Clock plugin

最も単純そうなプラグインを読んでいくシリーズ。 プラグインは Clock plugin。Panelプラグイン。配布はここ。 最も単純そうなDataSourceプラグインを読む記事は以下。 [clink url=\"https://ikuty.com/2020/11/14/grafana-code-read/\"] ダッシュボードに時計を表示できる。 ダッシュボードから設定をおこない表示に反映する機能を備えていて、 PanelプラグインのHelloWorldには良い感じ。 The Clock Panel can show the current time or a countdown and updates every second. Show the time in another office or show a countdown to an important event. 肝心のデータプロットに関する機能は無いので別途違うコンポーネントを読む。 インストール、ビルド 公式からインストールすると src が含まれない。 ソースコードをclone、buildすることにする。 初回だけgrafana-serverのrestartが必要。 # clone repository $ cd ~/ $ git clone https://github.com/grafana/clock-panel.git $ mv clock-panek /var/lib/grafana/plugins # install plugin $ yarn install $ yarn build # restart grafana-server $ sudo service grafana-server restart ディレクトリ・ファイル構成 ディレクトリ・ファイル構成は以下の通り。 clock-panel/ src/ ClockPanel.tsx ... プラグイン本体 module.ts ... プラグインのエントリポイント options.tsx plugin.json ... プラグインの設定ファイル types.ts ... TypeScript型定義 img/ ... 画像リソース clock.svg countdown1.png screenshot-clock-options.png screenshot-clocks.png screenshot-showcase.png external/ ... 外部ライブラリ moment-duration-formant.js エントリポイント ./module.ts の内容は以下の通り。 ClockPanel.tsxで定義済みのClockPanelクラスをexportしている。 options.tsxに記述したオプション画面関連のクラスを.setPanelOptions()を介して設定する。 import { PanelPlugin } from \'@grafana/data\'; import { ClockPanel } from \'./ClockPanel\'; import { ClockOptions } from \'./types\'; import { optionsBuilder } from \'./options\'; export const plugin = new PanelPlugin(ClockPanel).setNoPadding().setPanelOptions(optionsBuilder); 本体 (ClockPanel.tsx) ./ClockPanel.tsxを読んでいく。React+TypeScript。 import React, { PureComponent } from \'react\'; import { PanelProps } from \'@grafana/data\'; import { ClockOptions, ClockType, ZoneFormat, ClockMode } from \'./types\'; import { css } from \'emotion\'; // eslint-disable-next-line import moment, { Moment } from \'moment\'; import \'./external/moment-duration-format\'; interface Props extends PanelProps {} interface State { // eslint-disable-next-line now: Moment; } export function getTimeZoneNames(): string[] { return (moment as any).tz.names(); } // PureComponentクラスを派生させることでプラグイン用のパネルクラスを定義できる。 // パネルのプロパティは PanelProps型だが 当プラグイン用にProps型に拡張している。 export class ClockPanel extends PureComponent { timerID?: any; state = { now: this.getTZ(), timezone: \'\' }; //Componentのインスタンスが生成されDOMに挿入されるときに呼ばれる //DOM挿入後,1秒間隔で this.tick()の実行を開始する。 componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 // 1 second ); } //[非推奨] DOMから削除されるときに呼ばれる。古いのかな。 //this.tick()の実行を停止する。 componentWillUnmount() { clearInterval(this.timerID); } //DOM挿入後1秒間隔で呼ばれる。 //stateを更新する。 tick() { const { timezone } = this.props.options; this.setState({ now: this.getTZ(timezone) }); } //時刻フォーマットを取得する。 //時刻フォーマットはオプション設定画面で設定され props.optionsに渡される。 //渡される変数は clockType と timeSettings である。 clockTypeは 12時間/24時間のいずれか。 //12時間なら h:mm:ss A, 24時間なら HH:mm:ss。 //カスタムの場合,clockTypeがClockType.Customになる。 getTimeFormat() { const { clockType, timeSettings } = this.props.options; if (clockType === ClockType.Custom && timeSettings.customFormat) { return timeSettings.customFormat; } if (clockType === ClockType.H12) { return \'h:mm:ss A\'; } return \'HH:mm:ss\'; } // Return a new moment instnce in the selected timezone // eslint-disable-next-line getTZ(tz?: string): Moment { if (!tz) { tz = (moment as any).tz.guess(); } return (moment() as any).tz(tz); } //カウントダウン文字列を得る //設定値countdownSettings, timezone は props.options から得られる。 // getCountdownText(): string { const { now } = this.state; const { countdownSettings, timezone } = this.props.options; //カウントダウン終了時 設定された文字列 countdownSettings.endText を返す if (!countdownSettings.endCountdownTime) { return countdownSettings.endText; } //残り時間を計算。 const timeLeft = moment.duration( moment(countdownSettings.endCountdownTime) .utcOffset(this.getTZ(timezone).format(\'Z\'), true) .diff(now) ); let formattedTimeLeft = \'\'; //計算した残り時間が0以下であれば、設定された文字列 countdownSettings.endText を返す。 if (timeLeft.asSeconds() 0) { formattedTimeLeft = timeLeft.years() === 1 ? \'1 year, \' : timeLeft.years() + \' years, \'; previous = \'years\'; } // Y months (or Y month) if (timeLeft.months() > 0 || previous === \'years\') { formattedTimeLeft += timeLeft.months() === 1 ? \'1 month, \' : timeLeft.months() + \' months, \'; previous = \'months\'; } // Z days (or Z day) if (timeLeft.days() > 0 || previous === \'months\') { formattedTimeLeft += timeLeft.days() === 1 ? \'1 day, \' : timeLeft.days() + \' days, \'; previous = \'days\'; } // A hours (or A hour) if (timeLeft.hours() > 0 || previous === \'days\') { formattedTimeLeft += timeLeft.hours() === 1 ? \'1 hour, \' : timeLeft.hours() + \' hours, \'; previous = \'hours\'; } // B minutes (or B minute) if (timeLeft.minutes() > 0 || previous === \'hours\') { formattedTimeLeft += timeLeft.minutes() === 1 ? \'1 minute, \' : timeLeft.minutes() + \' minutes, \'; } // C minutes (or C minute) formattedTimeLeft += timeLeft.seconds() === 1 ? \'1 second \' : timeLeft.seconds() + \' seconds\'; return formattedTimeLeft; } //Zoneを表示するh4タグを作成して返す。Reactっぽい。 renderZone() { const { now } = this.state; const { timezoneSettings } = this.props.options; const { zoneFormat } = timezoneSettings; // ReactでCSSを書く作法。ヒアドキュメント // ロジックコードの中にHTML生成コードが混在して非常に見辛い。 const clazz = css` font-size: ${timezoneSettings.fontSize}; font-weight: ${timezoneSettings.fontWeight}; line-height: 1.4; `; let zone = this.props.options.timezone || \'\'; switch (zoneFormat) { case ZoneFormat.offsetAbbv: zone = now.format(\'Z z\'); break; case ZoneFormat.offset: zone = now.format(\'Z\'); break; case ZoneFormat.abbv: zone = now.format(\'z\'); break; default: try { zone = (this.getTZ(zone) as any)._z.name; } catch (e) { console.log(\'Error getting timezone\', e); } } return ( {zone} {zoneFormat === ZoneFormat.nameOffset && ( ({now.format(\'Z z\')}) ); } //Dateを表示するh3タグを返す。 renderDate() { const { now } = this.state; const { dateSettings } = this.props.options; const clazz = css` font-size: ${dateSettings.fontSize}; font-weight: ${dateSettings.fontWeight}; `; const disp = now.locale(dateSettings.locale || \'\').format(dateSettings.dateFormat); return ( {disp} ); } //Timeを返すh2タグを返す。 renderTime() { const { now } = this.state; const { timeSettings, mode } = this.props.options; const clazz = css` font-size: ${timeSettings.fontSize}; font-weight: ${timeSettings.fontWeight}; `; const disp = mode === ClockMode.countdown ? this.getCountdownText() : now.format(this.getTimeFormat()); return {disp}; } //React componentとしてrender()メソッドを実装する必要がある。 //CSSを整形してZone,Date,Timeを設定して返す。 render() { const { options, width, height } = this.props; const { bgColor, dateSettings, timezoneSettings } = options; const clazz = css` display: flex; align-items: center; justify-content: center; flex-direction: column; background-color: ${bgColor ?? \'\'}; text-align: center; `; return ( {dateSettings.showDate && this.renderDate()} {this.renderTime()} {timezoneSettings.showTimezone && this.renderZone()} ); } } 設定 (options.tsx) 設定画面側を読んでいく。 PanelOptionsEditorBuilder型の引数を取り、builderに対して機能実装していく。 機能実装というのは、つまり、ラジオボタンを追加したり、カスタムエディタを追加したり、など。 この実装で以下のような設定画面が表示される。(README.mdは古いので注意)。 Modeとして、時間をそのまま表示するTimeモードか、カウンドダウンモードかを二者択一で設定する。 背景色(BackgroundColor)をカラーピッカーで設定する。(ちなみにGrafanaV7では機能しない様子)。 addTimeFormat()メソッドにより、24h表示/12h表示/カスタム表示,FontSize,FontWeightの設定機能を追加する。 addTimeZone()メソッドにより,TimeZoneと表示有無の設定機能を追加する。 カウントダウンモードに設定すると、カウントダウン設定をおこなえるが, addCountdown()メソッドにより,カウントダウン設定を追加する。 決められた構文にしたがって欲しい機能を追加していくだけなので、 設定画面の実装が必要になったら必要な構文を調べて追加していくことになりそう。 import React from \'react\'; import { PanelOptionsEditorBuilder, GrafanaTheme, dateTime } from \'@grafana/data\'; import { ColorPicker, Input, Icon, stylesFactory } from \'@grafana/ui\'; import { css } from \'emotion\'; import { config } from \'@grafana/runtime\'; import { ClockOptions, ClockMode, ClockType, FontWeight, ZoneFormat } from \'./types\'; import { getTimeZoneNames } from \'./ClockPanel\'; export const optionsBuilder = (builder: PanelOptionsEditorBuilder) => { // Global options builder //ClockModeの二者択一。TimeかCountdownを選ばせる。 .addRadio({ path: \'mode\', name: \'Mode\', settings: { options: [ { value: ClockMode.time, label: \'Time\' }, { value: ClockMode.countdown, label: \'Countdown\' }, ], }, defaultValue: ClockMode.time, }) //背景色のカスタムエディタ。カラーピッカーから色を選ばせる。 .addCustomEditor({ id: \'bgColor\', path: \'bgColor\', name: \'Background Color\', editor: props => { const styles = getStyles(config.theme); let prefix: React.ReactNode = null; let suffix: React.ReactNode = null; if (props.value) { suffix = props.onChange(undefined)} />; } prefix = ( ); return ( { console.log(\'CLICK\'); }} prefix={prefix} suffix={suffix} /> ); }, defaultValue: \'\', }); // TODO: refreshSettings.syncWithDashboard addCountdown(builder); addTimeFormat(builder); addTimeZone(builder); addDateFormat(builder); }; //--------------------------------------------------------------------- // COUNTDOWN //--------------------------------------------------------------------- function addCountdown(builder: PanelOptionsEditorBuilder) { const category = [\'Countdown\']; builder .addTextInput({ category, path: \'countdownSettings.endCountdownTime\', name: \'End Time\', settings: { placeholder: \'ISO 8601 or RFC 2822 Date time\', }, defaultValue: dateTime(Date.now()) .add(6, \'h\') .format(), showIf: o => o.mode === ClockMode.countdown, }) .addTextInput({ category, path: \'countdownSettings.endText\', name: \'End Text\', defaultValue: \'00:00:00\', showIf: o => o.mode === ClockMode.countdown, }) .addTextInput({ category, path: \'countdownSettings.customFormat\', name: \'Custom format\', settings: { placeholder: \'optional\', }, defaultValue: undefined, showIf: o => o.mode === ClockMode.countdown, }); } //--------------------------------------------------------------------- // TIME FORMAT //--------------------------------------------------------------------- function addTimeFormat(builder: PanelOptionsEditorBuilder) { const category = [\'Time Format\']; builder .addRadio({ category, path: \'clockType\', name: \'Clock Type\', settings: { options: [ { value: ClockType.H24, label: \'24 Hour\' }, { value: ClockType.H12, label: \'12 Hour\' }, { value: ClockType.Custom, label: \'Custom\' }, ], }, defaultValue: ClockType.H24, }) .addTextInput({ category, path: \'timeSettings.customFormat\', name: \'Time Format\', description: \'the date formatting pattern\', settings: { placeholder: \'date format\', }, defaultValue: undefined, showIf: opts => opts.clockType === ClockType.Custom, }) .addTextInput({ category, path: \'timeSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'date format\', }, defaultValue: \'12px\', }) .addRadio({ category, path: \'timeSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, }); } //--------------------------------------------------------------------- // TIMEZONE //--------------------------------------------------------------------- function addTimeZone(builder: PanelOptionsEditorBuilder) { const category = [\'Timezone\']; const timezones = getTimeZoneNames().map(n => { return { label: n, value: n }; }); timezones.unshift({ label: \'Default\', value: \'\' }); builder .addSelect({ category, path: \'timezone\', name: \'Timezone\', settings: { options: timezones, }, defaultValue: \'\', }) .addBooleanSwitch({ category, path: \'timezoneSettings.showTimezone\', name: \'Show Timezone\', defaultValue: false, }) .addSelect({ category, path: \'timezoneSettings.zoneFormat\', name: \'Display Format\', settings: { options: [ { value: ZoneFormat.name, label: \'Normal\' }, { value: ZoneFormat.nameOffset, label: \'Name + Offset\' }, { value: ZoneFormat.offsetAbbv, label: \'Offset + Abbreviation\' }, { value: ZoneFormat.offset, label: \'Offset\' }, { value: ZoneFormat.abbv, label: \'Abbriviation\' }, ], }, defaultValue: ZoneFormat.offsetAbbv, showIf: s => s.timezoneSettings?.showTimezone, }) .addTextInput({ category, path: \'timezoneSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'font size\', }, defaultValue: \'12px\', showIf: s => s.timezoneSettings?.showTimezone, }) .addRadio({ category, path: \'timezoneSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, showIf: s => s.timezoneSettings?.showTimezone, }); } //--------------------------------------------------------------------- // DATE FORMAT //--------------------------------------------------------------------- function addDateFormat(builder: PanelOptionsEditorBuilder) { const category = [\'Date Options\']; builder .addBooleanSwitch({ category, path: \'dateSettings.showDate\', name: \'Show Date\', defaultValue: false, }) .addTextInput({ category, path: \'dateSettings.dateFormat\', name: \'Date Format\', settings: { placeholder: \'Enter date format\', }, defaultValue: \'YYYY-MM-DD\', showIf: s => s.dateSettings?.showDate, }) .addTextInput({ category, path: \'dateSettings.locale\', name: \'Locale\', settings: { placeholder: \'Enter locale: de, fr, es, ... (default: en)\', }, defaultValue: \'\', showIf: s => s.dateSettings?.showDate, }) .addTextInput({ category, path: \'dateSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'date format\', }, defaultValue: \'20px\', showIf: s => s.dateSettings?.showDate, }) .addRadio({ category, path: \'dateSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, showIf: s => s.dateSettings?.showDate, }); } const getStyles = stylesFactory((theme: GrafanaTheme) => { return { colorPicker: css` padding: 0 ${theme.spacing.sm}; `, inputPrefix: css` display: flex; align-items: center; `, trashIcon: css` color: ${theme.colors.textWeak}; cursor: pointer; &:hover { color: ${theme.colors.text}; } `, }; });

default eye-catch image.

Grafanaプラグインを読んでいく – simpod-json-datasource plugin

最も単純そうなDataSourceプラグインを読んでいく。 プラグインは simpod-json-datasource plugin。 配布はここ。 JSONを返す任意のURLにリクエストを投げて結果を利用できるようにする。 TypeScriptで書かれている。 The JSON Datasource executes JSON requests against arbitrary backends. JSON Datasource is built on top of the Simple JSON Datasource. It has refactored code, additional features and active development. install インストール方法は以下の通り。 初回だけgrafana-serverのrestartが必要。 # install plugin $ grafana-cli plugins install simpod-json-datasource # restart grafana-server $ sudo service grafana-server restart build ビルド方法は以下の通り。 yarn一発。 # build $ cd /var/lib/grafana/plugins/simpod-json-datasource $ yarn install $ yarn run build 設定 データソースの追加で、今回インストールした DataSource/JSON (simpod-json-datasource)を 選択すると、プラグインのコンフィグを変更できる。 要求するURL一覧 URLはBaseURL。 このプラグインはURLの下にいくつかのURLが存在することを想定している。 つまり、それぞれのURLに必要な機能を実装することでプラグインが機能する。 用語については順に解説する。 必須 Method Path Memo GET / ConfigページでStatusCheckを行うためのURL。200が返ればConfigページで「正常」となる。 POST /search 呼び出し時に「利用可能なメトリクス」を返す。 POST /query 「メトリクスに基づくデータポイント」を返す。 POST /annotations 「アノテーション」を返す。 オプショナル Method Path Memo POST /tag-keys 「ad-hocフィルタ」用のタグのキーを返す。 POST /tag-values 「ad-hocフィルタ」用のタグの値を返す。 メトリクス Grafanaにおいて、整理されていないデータの中から\"ある観点\"に従ったデータを取得したいという ユースケースをモデル化している. 例えば、サーバ群の負荷状況を監視したいというケースでは「サーバ名」毎にデータを取得したいし、 センサ群から得られるデータを監視したいというケースでは「センサID」毎にデータを取得したい. これらの観点は、Grafanaにおいて「メトリクス」または「タグ」として扱われる。 センサAからセンサZのうちセンサPとセンサQのデータのみ表示したい、という感じで使う。 ここで現れるセンサAからセンサZが「メトリクス」である。 クエリ変数のサポート (metricFindQuery) 「クエリ変数」は、「メトリクス」を格納する変数である。 データソースプラグインがクエリ変数をサポートするためには、 DataSourceApi クラスの metricFindQuery を オーバーライド する. string型のqueryパラメタを受け取り、MetricFindValue型変数の配列を返す. 要は以下の問答を定義するものである。 - 質問 : 「どんなメトリクスがありますか?」 - 回答 : 「存在するメトリクスはxxx,yyy,zzzです」 ちなみに、metricFindQueryが受け取るqueryの型は、 metricFindQueryを呼び出すUIがstring型で呼び出すからstring型なのであって、 UIを変更することで別の型に変更することができる。 以下、simpod-json-datasource の metricFindQuery の実装。 // MetricFindValue interfaceはtext,valueプロパティを持つ // { \"label\":\"upper_25\", \"value\": 1}, { \"label\":\"upper_50\", \"value\": 2} のような配列を返す. metricFindQuery(query: string, options?: any, type?: string): Promise { // Grafanaが用意する interpolation(補間)関数 // query として \"query field value\" が渡されたとする // interpolated は { \"target\": \"query field value\" }のようになる. const interpolated = { type, target: getTemplateSrv().replace(query, undefined, \'regex\'), }; return this.doRequest({ url: `${this.url}/search`, data: interpolated, method: \'POST\', }).then(this.mapToTextValue); } // /searchから返ったJSONは2パターンある // 配列 ([\"upper_25\",\"upper_50\",\"upper_75\",\"upper_90\",\"upper_95\"])か、 // map ([ { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} ]) // これらを MetricFindValue型に変換する mapToTextValue(result: any) { return result.data.map((d: any, i: any) => { // mapの場合 if (d && d.text && d.value) { return { text: d.text, value: d.value }; } // 配列の場合 if (isObject(d)) { return { text: d, value: i }; } return { text: d, value: d }; }); } 以下、simpod-json-datasource が メトリクスを取得する部分のUI。 FormatAs, Metric, Additional JSON Dataの各項目が選択可能で、 各々の変数がViewModelとバインドされている. コードは以下。React。 FormatAsのセレクトボックスのOnChangeでsetFormatAs()が呼ばれる。 MetricのセレクトボックスのOnChagneでsetMetric()が呼ばれる。 Additional JSON DataのonBlurでsetData()が呼ばれる。 import { QueryEditorProps, SelectableValue } from \'@grafana/data\'; import { AsyncSelect, CodeEditor, Label, Select } from \'@grafana/ui\'; import { find } from \'lodash\'; import React, { ComponentType } from \'react\'; import { DataSource } from \'./DataSource\'; import { Format } from \'./format\'; import { GenericOptions, GrafanaQuery } from \'./types\'; type Props = QueryEditorProps; const formatAsOptions = [ { label: \'Time series\', value: Format.Timeseries }, { label: \'Table\', value: Format.Table }, ]; export const QueryEditor: ComponentType = ({ datasource, onChange, onRunQuery, query }) => { const [formatAs, setFormatAs] = React.useState<selectableValue>( find(formatAsOptions, option => option.value === query.type) ?? formatAsOptions[0] ); const [metric, setMetric] = React.useState<selectableValue>(); const [data, setData] = React.useState(query.data ?? \'\'); // 第2引数は依存する値の配列 (data, formatAs, metric). // 第2引数の値が変わるたびに第1引数の関数が実行される. React.useEffect(() => { // formatAs.value が 空なら何もしない if (formatAs.value === undefined) { return; } // metric.value が 空なら何もしない if (metric?.value === undefined) { return; } // onChange(..)を実行する onChange({ ...query, data: data, target: metric.value, type: formatAs.value }); onRunQuery(); }, [data, formatAs, metric]); // Metricを表示するセレクトボックスの値に関数がバインドされている. // 関数はstring型のパラメタを1個とる. const loadMetrics = (searchQuery: string) => { // datasourceオブジェクトの metricFindQuery()を呼び出す. // 引数はsearchQuery. 戻り値はMetricFindValue型の配列(key/valueが入っている). // 例えば { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} return datasource.metricFindQuery(searchQuery).then( result => { // { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} から // { \"label\": \"upper_25\", \"value\": 1}, { \"label\": \"upper_75\", \"value\": 2} へ const metrics = result.map(value => ({ label: value.text, value: value.value })); // セレクトボックスで選択中のMetricを取得し setMetric に渡す setMetric(find(metrics, metric => metric.value === query.target)); return metrics; }, response => { throw new Error(response.statusText); } ); }; return ( <> <div className=\"gf-form-inline\"> <div className=\"gf-form\"> <select prefix=\"Format As: \" options={formatAsOptions} defaultValue={formatAs} onChange={v => { setFormatAs(v); }} /> </div> <div className=\"gf-form\"> <asyncSelect prefix=\"Metric: \" loadOptions={loadMetrics} defaultOptions placeholder=\"Select metric\" allowCustomValue value={metric} onChange={v => { setMetric(v); }} /> </div> </div> <div className=\"gf-form gf-form--alt\"> <div className=\"gf-form-label\"> <label>Additional JSON Data </div> <div className=\"gf-form\"> <codeEditor width=\"500px\" height=\"100px\" language=\"json\" showLineNumbers={true} showMiniMap={data.length > 100} value={data} onBlur={value => setData(value)} /> </div> </div> </> ); // } }; クエリ変数から値を取得 (queryの実装) 続いて、選択したメトリクスのデータ列を得るための仕組み. query()インターフェースを実装する. QueryRequest型の引数を受け取る. 引数には、どのメトリクスを使う、だとか、取得範囲だとか、様々な情報が入っている。 QueryRequest型の引数を加工した値を、/query URLにPOSTで渡す. /query URLから戻ってきた値は DataQueryResponse型と互換性があり、query()の戻り値として返る. // UIから送られてきたQueryRequest型の変数optionsを処理して /query に投げるJSONを作る。 // QueryRequest型は当プラグインがGrafanaQuery型interfaceを派生させて作った型。 query(options: QueryRequest): Promise { const request = this.processTargets(options); // 処理した結果、targets配列が空なら空を返す。 if (request.targets.length === 0) { return Promise.resolve({ data: [] }); } // JSONにadhocFiltersを追加する // @ts-ignore request.adhocFilters = getTemplateSrv().getAdhocFilters(this.name); // JSONにscopedVarsを追加する options.scopedVars = { ...this.getVariables(), ...options.scopedVars }; // /queryにJSONをPOSTで投げて応答をDataQueryResponse型で返す。 return this.doRequest({ url: `${this.url}/query`, data: request, method: \'POST\', }); } // UIから送られてきたQueryRequest型変数optionsのtargetsプロパティを加工して返す // processTargets(options: QueryRequest) { options.targets = options.targets .filter(target => { // remove placeholder targets return target.target !== undefined; }) .map(target => { if (target.data.trim() !== \'\') { // JSON様の文字列target.data をJSONオブジェクト(key-value)に変換する // reviverとして関数が指定されている // valueが文字列であった場合にvalue内に含まれる変数名を置換する // 置換ルールは cleanMatch メソッド target.data = JSON.parse(target.data, (key, value) => { if (typeof value === \'string\') { return value.replace((getTemplateSrv() as any).regex, match => this.cleanMatch(match, options)); } return value; }); } // target.targetには、変数のプレースホルダ($..)が存在する. // grafanaユーザが入力した変数がoptions.scopedVarsに届くので、 // target.target内のプレースホルダをoptions.scopedVarsで置換する。 // 置換後の書式は正規表現(regex) if (typeof target.target === \'string\') { target.target = getTemplateSrv().replace(target.target.toString(), options.scopedVars, \'regex\'); } return target; }); return options; } // cleanMatch cleanMatch(match: string, options: any) { // const replacedMatch = getTemplateSrv().replace(match, options.scopedVars, \'json\'); if ( typeof replacedMatch === \'string\' && replacedMatch[0] === \'\"\' && replacedMatch[replacedMatch.length - 1] === \'\"\' ) { return JSON.parse(replacedMatch); } return replacedMatch; } TypeScriptに明るくない場合、以下を参照。 - Typescript-array-filter - 【JavaScript】map関数を用いたおしゃれな配列処理 - TypeScript - String replace() JavaScriptのreplaceとGrafanaのreplaceが混在していて、 IDEが無いとかなり厳しい感じ。 - Interpolate variables in data source plugins - Advanced variable format options query()のパラメタについて、 Grafanaのデフォだとstring型で来るのだが、プラグインが必要に応じて好きに変更できる。 つまりquery()に値を投げる部分をプラグインが変更できるため、変更に応じて受け側も変更する。 当プラグインはDataQueryRequestインターフェースを派生させた型を用意している。 - DataQueryRequest interface getTemplateSrv()はGrafanaのユーティリティ関数。 - getTemplateSrv variable - Github getTemplateSrv - Variable syntax 現在アクティブなダッシュボード内の変数が全て得られる。 * Via the TemplateSrv consumers get access to all the available template variables * that can be used within the current active dashboard. import { getTemplateSrv } from ‘@grafana/runtime’; const templateSrv = getTemplateSrv(); const variablesProtected = templateSrv.getVariables(); const variablesStringfied = JSON.stringify( variablesProtected ); const variables = JSON.parse( variablesStringfied ); URLに投げるJSONは例えば以下の通り。 { \"app\": \"dashboard\", \"requestId\": \"Q171\", \"timezone\": \"browser\", \"panelId\": 23763571993, \"dashboardId\": 1, \"range\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\", \"raw\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\" } }, \"timeInfo\": \"\", \"interval\": \"50ms\", \"intervalMs\": 50, \"targets\": [ { \"refId\": \"A\", \"data\": \"\", \"target\": \"upper_50\", \"type\": \"timeseries\", \"datasource\": \"JSON-ikuty\" } ], \"maxDataPoints\": 1058, \"scopedVars\": { \"variable1\": { \"text\": [ \"upper_50\" ], \"value\": [ \"upper_50\" ] }, \"__interval\": { \"text\": \"50ms\", \"value\": \"50ms\" }, \"__interval_ms\": { \"text\": \"50\", \"value\": 50 } }, \"startTime\": 1605108668062, \"rangeRaw\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\" }, \"adhocFilters\": [] } URLから返る値は例えば以下の通り. [ { \"target\": \"pps in\", \"datapoints\": [ [ 622, 1450754160000 ], [ 365, 1450754220000 ] ] }, { \"target\": \"pps out\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] }, { \"target\": \"errors out\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] }, { \"target\": \"errors in\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] } ] アノテーションのサポート (annotationQueryの実装) Grafanaには、ユーザがグラフの中に注意を喚起するラベル(またはラベルの範囲)を 設定する機能がある。 ユーザが自力で書くだけでなく、プラグインがアノテーションを提供することもできる。 simpod-json-datasourceプラグインは、アノテーションを提供する機能を有する。 公式の説明はこちら。 例えば下図で薄く赤くなっている部分が プラグインによるアノテーション。 実装方法は以下。 - アノテーションサポートを有効にする - annotationQueryインターフェースを実装する - アノテーションイベントを作成する アノテーションサポートを有効にするためには、 plugin.json に以下を追加する。 { \"annotations\": true } simpod-json-datasourceプラグインのannotationQueryの実装は以下。 annotationQuery( options: AnnotationQueryRequest ): Promise { const query = getTemplateSrv().replace(options.annotation.query, {}, \'glob\'); const annotationQuery = { annotation: { query, name: options.annotation.name, datasource: options.annotation.datasource, enable: options.annotation.enable, iconColor: options.annotation.iconColor, }, range: options.range, rangeRaw: options.rangeRaw, variables: this.getVariables(), }; return this.doRequest({ url: `${this.url}/annotations`, method: \'POST\', data: annotationQuery, }).then((result: any) => { return result.data; }); } プラグインからURLにPOSTのBODYで渡ってくるデータは以下。 { \"annotation\": { \"query\": \"hogehoge\", \"name\": \"hoge\", \"datasource\": \"JSON-ikuty\", \"enable\": true, \"iconColor\": \"rgba(255, 96, 96, 1)\" }, \"range\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\", \"raw\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\" } }, \"rangeRaw\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\" }, \"variables\": { \"variable1\": { \"text\": [ \"All\" ], \"value\": [ \"upper_25\", \"upper_50\", \"upper_75\", \"upper_90\", \"upper_105\" ] } } } これに対して以下の応答を返すとイメージのようになる。 以下はAnnotationEvent型と型が一致している。 isRegionがtrueの場合、timeEndを付与することでアノテーションが領域になる。 isRegionがfalseの場合、timeEndは不要。 tagsにタグ一覧を付与できる。 [ { \"text\": \"text shown in body\", \"title\": \"Annotation Title\", \"isRegion\": true, \"time\": \"1450754170000\", \"timeEnd\": \"1450754180000\", \"tags\": [ \"tag1\" ] } ]

default eye-catch image.

Redshift テーブル設計のベストプラクティス

どのようにテーブル設計するとパフォーマンスを得られるか. 公式がベストプラクティスを用意している. Redshiftのベストプラクティスが先にあってER図が後なのか、 ER図に対してベストプラクティスを適用するのか、 実際は行ったり来たりするようなイメージ. ER図とは別に何を考慮すべきなのか読み進めていく. [arst_toc tag=\"h4\"] ソートキー テーブル作成時に1つ以上の列をソートキーとして設定できる. 設定するとソートキーに準じたソート順でディスクに格納される. ソートキーに関するベストプラクティスは以下の通り. 最新のデータを得たい場合はタイムスタンプ列をソートキーにする. 1つの列に対してwhere句による範囲指定or等価指定をおこなう場合はその列をソートキーにする. ディメンションテーブルを頻繁に結合する場合は結合キーをソートキーにする. ファクトテーブルを中心にディメンションテーブルが4つある構造があるとする. ファクトテーブルにはディメンションテーブルのPKが入り関連している. また、ファクトテーブルに日付カラムがあり、常に最新のレコードが欲しいとする. ベストプラクティスによると、 各テーブルの各カラムに以下のようにソートキーを設定する. 分散スタイル クエリの実行を複数のクラスタ(コンピューティングノード、スライス)で実行するために、 それらに1)データを配信して 2)計算させて 3)結合、集計する というステップが必要になる。 最後のステップ3を達成するために、データの再び配ることが必要となる。 全体として最適となるように、1),2),3)の効率を高める必要があるが、 あらゆるデータ、条件について同じ戦略で最高の効率を得ることはできず、 設計者が戦略を指定するパラメタとなっている。 この戦略を分散スタイルと呼んでいる. 分散スタイルとして以下の3通りが用意されている. 各々だけ読むとさっぱり意味がわからないが、結局のところ再分散のコストをいかに減らすか、 というところに着目すると合点がいく. EVEN 分散 特定の列に含まれている値と関係なくラウンドロビンで複数のスライス間で行を分散させる. テーブルが結合に関与していない場合や、キー分散、ALL分散のどちらが良いかわからない場合に指定する. キー分散 キー分散のキーとは結合キーのこと. 特定の列に含まれている値に従って複数のスライスに行を分散させる.キーが同じということは「同じデータ」であり「同じデータ」達を同じスライスに分散させる意味がある. 共通の列からの一致する値が同じスライスにまとめられるよう努力する. ALL 分散 テーブル全体のコピーが全てのノードに分散される. EVEN分散、キー分散によってテーブルの一部が各ノードに配置されているときにALL分散を行うことでテーブルが関与しているあらゆる結合で全ての行が確実にコロケーションされる. 何が嬉しいのかわかりづらいが、EVEN分散やキー分散では、クエリ実行に伴って、再び必要なデータをコピーする(再分散する)必要が発生する可能性が生じる.ALL分散であればその可能性がなくなる. AUTO 分散 (デフォルト) テーブルデータのサイズに基づいて最適な分散スタイルを割り当てる. まず小さなテーブルにALL分散を設定し,テーブルが大きくなるとEVEN分散に切り替える. 分散スタイルを明示的に設定しないとAUTO分散になる. まず、ファクトテーブル関連する1つのテーブルの共通の列に基づいて分散させる. 関連するテーブルの選び方の観点は大きさで最もレコード数が大きいテーブルを選択する. 以下の構造では、ファクトテーブルとディメンションテーブル1が dim1_keyというキーを使って結合している. そこで, ファクトテーブルのdim1_key、ディメンションテーブル1のdim1_keyを分散キーとして採用する.(緑) ここまでで、dim1_keyの値が一致するレコードが同じスライスにコロケーションされる. キー分散に使うキーは1組のみ. 残りのテーブルについてはEVEN分散かALL分散を用いる. 選び方は上記の通り. テーブルのサイズが小さいのであれば、ALL分散により再分配の可能性がなくなり選びやすい. 圧縮エンコーディング 通常のRDBのように行方向の固まりを記録する場合、各列の値は型や値の傾向がまちまちであるため、 一様に圧縮しようとしても高い圧縮率を得られない. 対して、列方向の固まりを記録する場合、各列の型は同じだし値の傾向が似ていることが多いため、 高い圧縮率を得られる可能性がある. ただし、値の傾向により圧縮アルゴリズムを選択する必要がある. 公式で挙げられているアルゴリズム. 結局試してみないとわからない、というのはある. (Zstandard強すぎないか?) raw エンコード 圧縮をおこなわない. ソートキーが設定されているときはrawエンコードが設定される BOOLEAN、REAL,DOUBLE PRECISION型もraw. AZ64 エンコード Amazon 独自の圧縮エンコードアルゴリズム より小さなデータ値のグループを圧縮し、並列処理に SIMD (Single Instruction Multiple Data) 命令を使用する 数値、日付、および時刻データ型のストレージを大幅に節約する バイトディクショナリエンコード バイトディクショナリエンコード ディスク上の列値のブロックごとに、一意の値の個別のディクショナリを作成する 列に含まれる一意の値の数が制限されている場合に非常に効果的 列のデータドメインが一意の値 256 個未満である場合に最適 CHAR 列に長い文字列が含まれる場合に特に空間効率が高まる VARCHAR 列に対しては、LZO などの BYTEDICT 以外のエンコードを使用する デルタエンコード 列内の連続する値間の差を記録することにより、データを圧縮 日時列にとって非常に有用 差が小さいときに特に有用 LZO エンコード 非常に長い文字列を格納する CHAR および VARCHAR 列、特に製品説明、ユーザーコメント、JSON 文字列などの自由形式テキストに適している Mostly エンコード 列のデータ型が、格納された大部分の値で必要なサイズより大きい場合に有用. たとえば、INT2 列などの 16 ビット列を 8 ビットストレージに圧縮できる ランレングスエンコード 連続して繰り返される値を、値と連続発生数 (実行の長さ) から成るトークンに置き換える データ値が連続して繰り返されることが多いテーブルに最適 たとえば、テーブルがこれらの値でソートされている場合など Text255 および Text32k エンコード 同じ単語が頻繁に出現する VARCHAR 列を圧縮する場合に有用 Zstandard エンコード 多様なデータセット間で非常にパフォーマンスのいい高圧縮比率を提供 製品説明、ユーザーのコメント、ログ、JSON 文字列など、長さがさまざまな文字列を保存する CHAR および VARCHAR 列に対して有用 圧縮エンコーディングをテストするためには、 各アルゴリズムで差が出るように大量のデータを用意する必要がある. 公式には、テストするために大量のデータを用意することは難しいので デカルト積ででっち上げる手法が案内されている. 例えば、こんな感じにデータをでっちあげる. create table cartesian_venue( venueid smallint not null distkey sortkey, venuename varchar(100), venuecity varchar(30), venuestate char(2), venueseats integer ); insert into cartesian_venue select venueid, venuename, venuecity, venuestate, venueseats from venue, listing; このうち、venunameに対して各エンコーディングアルゴリズムを適用して格納するデータを作る. create table encodingvenue ( venueraw varchar(100) encode raw, venuebytedict varchar(100) encode bytedict, venuelzo varchar(100) encode lzo, venuerunlength varchar(100) encode runlength, venuetext255 varchar(100) encode text255, venuetext32k varchar(100) encode text32k, venuezstd varchar(100) encode zstd); insert into encodingvenue select venuename as venueraw, venuename as venuebytedict, venuename as venuelzo, venuename as venuerunlength, venuename as venuetext32k, venuename as venuetext255, venuename as venuezstd from cartesian_venue; 知りたいことは、encodingvenueの各列で実際に使われているディスク容量. 以下のようにして各列で使用される1 MBのディスク ブロック数を比較するらしい. rawが203に対してBYTEDICTが10. つまりBYTEDICTにより20:1の圧縮率を得られた例. select col, max(blocknum) from stv_blocklist b, stv_tbl_perm p where (b.tbl=p.id) and name =\'encodingvenue\' and col < 7 group by name, col order by col; col | max -----+----- 0 | 203 1 | 10 2 | 22 3 | 204 4 | 56 5 | 72 6 | 20 (7 rows) まとめ 公式のベストプラクティスを追ってみた. 面倒だけれども結構力技で出来ているなという印象. 与えられたスタースキーマからある程度決まったやり方でパラメタを選択できそう. 実データでやったら迷うことは必至w ソートキーと分散スタイルの選択は分散コストに影響する. 圧縮エンコーディングの選択はディスクストレージに影響する. 理解したら実際に試行錯誤していくしかないイメージ.

default eye-catch image.

postgresユーザのホームディレクトリ

ubuntuの場合、postgresユーザのホームディレクトリは /var/lib/postgresql 。 例えば .pgpass をここに置くと、postgres ユーザで psql を実行した場合でも読んでくれる。 ホームディレクトリが無い理由 プログラムを実行するために作成されたユーザとコンソールログインするユーザは扱いが違う。 例えば nginx、mysql のように PostgreSQL の実行ユーザである postgres のホームディレクトリは、 PostgreSQL の maintainer が決める。 /home/postgres というディレクトリは作られない。 PostgreSQLは, root の代わりに postgres ユーザを使ってり様々な処理をおこなう. ホームディレクトリはどこ? PostgreSQLのインストールディレクトリがホームディレクトリ。 ubuntuの場合、PostgreSQLは /var/lib/postgresql にインストールされていて、 /var/lib/postgresql がホームディレクトリ。 データディレクトリの調べ方 PostgreSQLのデータベースファイルのレイアウトによると、 データディレクトリに全てのファイルが格納される。 postgres の起動パラメタとして データディレクトリ が指定されている。 (-D) 以下の例だと、/var/lib/postgresql/9.5/main。 $ ps ax | grep postgres | grep -v postgres: 1377 ? S 0:01 /usr/lib/postgresql/9.5/bin/postgres -D /var/lib/postgresql/9.5/main -c config_file=/etc/postgresql/9.5/main/postgresql.conf 10600 pts/0 T 0:00 sudo -s -u postgres 11930 pts/0 S+ 0:00 grep --color=auto postgres 別の方法として、psql から SHOW data_directory を実行することで データディレクトリを得られる。 やはり、/var/lib/postgresql/9.5/main。 $ sudo -s -u postgres $ psql postgres=# SHOW data_directory; data_directory ------------------------------ /var/lib/postgresql/9.5/main (1 row) ホームディレクトリの下の階層にデータディレクトリが出来ている。 /home/以下が作られないユーザについて(Stackoverflow) Why Directory for postgres user does not appear inside the HOME directory in linux with other users? [closed] That there is a dedicated user for PostgreSQL is a security measure, so the DB processes can run with that user\'s (limited) priviledges instead of running as root. Whether or not you can actually log on with that user, and what that user\'s home directory should be, is the decision of the package maintainer / the Linux distribution in question. Since the postgresql user should not be (ab-) used as just another user (with own desktop settings, user data etc.), I wouldn\'t question the wisdom of not giving it a home, but rather why he is enabled to log in in the first place. Edit: Being ignorant of the fine print of PostgreSQL, and a bit confused by the wording of your question, I argued the general case. Ignacio pointed out that you had to actually break the system (unlock the user\'s password with root priviledges) to even be able to log in as postgresql user. So the answer can be phrased even simpler: The user does not have a directory in /home because you are not supposed to ever log in as that user. It\'s for running the database processes without root priviledges, nothing else. (Note that you could, using the same technique, log in as user man, or user lp, or user mail. You could, but it wouldn\'t make sense, and unlocking those user\'s passwords actually weakens the security of your system.)

default eye-catch image.

接続中のセッションを全部切る方法

セッション毎にプロセスが動いている。 pg_terminate_backend()を使ってプロセスを落とせば良い。 動いているプロセスを落とせばセッションは切れる。 killで落とすと上手くいかないので注意。 基本形 基本形は以下の通り。 $ sudo -s -u postgres $ psql postgres=> select pg_terminate_backend({プロセスID}); 通常、セッションは複数存在するため、切りたいセッションのプロセスIDを選択して pg_terminate_backend()に渡す必要がある。 自分以外全部切る 生きているセッションをpg_terminate_backend()(後述)を使って探し、 pg_terminate_backend()に食わせて落とす。 自分自身のpidは pg_backend_pid() で得られる。 $ sudo -s -u postgres $ psql postgres=> SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = \'DB名\' AND pid pg_backend_pid(); datnameはTypoではない! 動的統計情報ビュー 上で使っているpg_stat_activityは動的統計情報ビューというビルトインのビューの一つ。 27.2.2. 統計情報の表示。 サーバ当たり1行の形式で、 状態や現在の問い合わせ等のプロセスの現在の活動状況に関連した情報を表示する。 取れるデータ達は以下の通り。PostgresSQLのバージョンによって異なるが、 よく使いそうなものは変わらなそう。使う場合には要注意。 postgres=> d pg_stat_activity; View \"pg_catalog.pg_stat_activity\" Column | Type | Modifiers ------------------+--------------------------+----------- datid | oid | ; バックエンドが接続するデータベースのOID datname | name | ; バックエンドが接続するデータベースの名前 pid | integer | ; バックエンドのプロセスID usesysid | oid | ; バックエンドにログインしたユーザの識別子 usename | name | ; バックエンドに接続したユーザの名前 application_name | text | ; バックエンドに接続したアプリケーションの名前 client_addr | inet | ; バックエンドに接続したクライアントのIPアドレス client_hostname | text | ; client_addrの逆引き検索により報告された、接続クライアントのホスト名 client_port | integer | ; クライアントがバックエンドとの通信に使用するTCPポート backend_start | timestamp with time zone | ; プロセスが開始、つまりクライアントがサーバに接続した時刻 xact_start | timestamp with time zone | ; プロセスの現在のトランザクションが開始した時刻 query_start | timestamp with time zone | ; 現在有効な問い合わせが開始した時刻 state_change | timestamp with time zone | ; stateの最終変更時刻 wait_event_type | text | ; he type of event for which the backend is waiting wait_event | text | ; Wait event name if backend is currently waiting, otherwise NULL. state | text | ; Current overall state of this backend backend_xid | xid | ; もしあれば、このバックエンドの最上位のトランザクション識別子 backend_xmin | xid | ; 現在のバックエンドのxmin query | text | ; バックエンドの最も最近の問い合わせテキスト backend_type | text | ; Type of current backend

default eye-catch image.

TableuServer認定資格

この記事 Tableau未経験者がTableauを扱う仕事をすることになったのと, 合わせてServer Certified Associate資格の取得が必要になったためいつものごとく学びの軌跡を記録していく. マルチノード高可用性のための設計を学ぶというモチベ 潰しが効く系ではないのだが, 実際にインストールして設定しようとすると知らなければならないことが並んでいる. 使わないけれど知識として並べてある, という一部のベンダー資格とは違う印象がある. 通常,いきなりマルチノードで高可用性が..とかにはならないはずだがどこかで入門レベルが存在しなければならない. 最小構成/シングルノード構成/マルチノード構成へとスケールできるアーキテクチャになっていることを理解することが結局のところスケールを前提とした入門になり得ると思う. あるシステムのシステム構成がここまで明らかになっているのも珍しい印象. マルチノードと高可用性実現のため, レイヤやコンポーネントが想像以上に分離していて, スケールさせる際の自由度の素になっている. スケールのための設計をゼロベースで学んだ経験がないため,自分が自作するレベルのシステムのアーキテクチャではここまで分解しないな,というのは当然あったが, 正直かなり設計の勉強になった. 対策 過去問が公開されていたり,公式参考書があったりはしないため,試験のための習得にはなりづらい. マルチノード化による高可用性を実現するためのアーキテクチャを学ぶ機会と捉えれば,結構モチベになると思う. (そんな人いるんでしょうか..w) 試験のシラバスが項目単位で並んでいる. この項目を自分の言葉で説明できることが当面のゴール. 言葉で説明する深さについては,暗記が不要という難易度調整が入っておりそれほど深くなくてよいと思う. 結果として「ロジックを自分の言葉で説明できること」がゴールになり得る. 個人的な感想を書くと,もはやかなり覚えてしまっている気はする. 当面は「学びの軌跡」。合格したときに「合格体験記」としてまとめなおす. この投稿を入り口として各詳細を別記事として書いていく予定. 1セクションを1記事として扱い合計4から5記事で詳細を完結する予定. 当面は合格体験記ではなく勉強の記録なので、資格試験合格のために ここに流入してしまった人は抜けて他読んだ方が学びになると思います. TableauDesktop TableauServerはTableauDesktopで作ったワークブック等をサーバ上で共有する仕組みであることから, 多くの記述においてTableauDesktopの知識が前提となる.TableauDesktopで一通り何ができるかを学んだ方が良い.ポチポチで全てやれちゃう範囲と深さがあればTableauServerの理解には不足ないと思う. 試験の概要 Server Certified Associate試験の概要は以下の通り。 合格基準が75%。ベンダー資格としてはこんなものか、若干高いかも。 択一だけでなく多肢選択も含まれるので、結構落とせない。 問題数80問に対して90分。問題多い。合格体験記を見る限り時間が不足しがちな試験。 試験範囲に以下みたいな記述がある。難易度の調整パラメータかな。 組織は、タスクを効果的かつ効率的に遂行することを、当然のように従業員に期待するようになりました。Tableau は、時間が成功に欠かせないコンピテンシーであると考えており、そのためこの試験にも時間制限が設けられています。 Tableau ServerはLinux,WindowsServerいずれのインストールも対応しているが試験はWindows。 個人的にはLinuxが良いのだけど仕方がない。OS違うのに試験同じにできるのかね..。 オンラインヘルプが参照可能。だが時間無いので記憶の照合くらいにしか使えなさそう。 どこに書いてあるかを記憶しておくことが必要。 Tableu資格の特徴は, 試験開始までの準備でリモート先の試験官と英会話が必要なこと。 提供言語として日本語が用意されているが,これは問題が日本語であるということ. 試験時間: 90 分 合格基準: 正答率 75% 問題数: 80 得点: 自動採点 設問形式: 択一、複数回答、正誤 提供言語: 英語、日本語、中国語 (簡体字)、ドイツ語、フランス語、ポルトガル語 (ブラジル)、スペイン語 (インターナショナル) プラットフォーム: Tableau Server がインストールされた Windows 仮想マシン オンラインヘルプ: 参照可能 Readingはそこそこ問題ないのでこっちは良いのだけど,試験官とSpeaking/Listeningとか不安。 試験本体までのやりとりは以下のような感じらしい。むー。 試験官にweb電話をかける ガイダンスが始まる web通話環境に問題ないことの確認が入る 本人確認:受ける試験が正しいか、パスポートをwebカメラに見せて欲しい,etc 環境確認:PCは充電器に接続されているか、部屋に一人でいるか、机の上を見せて欲しい、途中退出はダメ,etc リモートで指定されたPC環境にログインされ試験開始 なんとか試験本体までたどりつかないと。 ここから先は普通のベンダー試験。 試験範囲 記事執筆時点の試験範囲は以下。 評価するスキル 準備セクション。全体の20%。把握するためにペタペタ貼っていく。 ところどころ,当製品と関係ない一般的な知識が書かれているな..。 ユーザーエクスペリエンス - ユーザーインターフェイス - ナビゲーション トポロジ - クライアントコンポーネントを特定する - サーバーコンポーネントを特定する - 連動する仕組みを説明する バージョン - 以下を理解する: - Tableau Server の現行バージョンを特定する方法 - Tableau Server の最新リリースをどこで入手できるか - Tableau Server のリリースノートをどこで参照できるか ハードウェア最小要件 - 以下を理解する: - RAM 要件 - CPU 要件 - ハードディスク要件 ソフトウェア要件 - サポートされるオペレーティングシステムを列挙する - 以下を理解する: - ブラウザ要件 - メールアラートのオプション - ウィルス対策の懸念事項 - SMTP サーバーを特定する - 起こりうるポートの問題を知る - 専用サーバーの目的と利点を説明する - クラウドで稼働させる際の検討事項を特定する ライセンス発行 - ユーザーベースライセンスを理解する - 異なるライセンスタイプを説明する - ライセンスタイプがどのようにサイトロールにマップするか説明する サーバープロセス - Tableau サービスマネージャーとTableau Serverプロセスをそれぞれ説明する - 以下を理解する: - インストール直後の既定のプロセス数 - 複数インスタンスのプロセス - プロセス間のワークフロー - 分散環境と高可用性環境におけるプロセス - ロードバランサーの目的 データソースの特定 - 必要なポートを特定する - 必要なデータベースドライバーを特定する - 以下の相違点を理解する: - ファイル、リレーショナル、キューブ - 抽出とライブ接続 - パブリッシュされたデータソースの利点を説明する インフラストラクチャネットワーク - ネットワークレイテンシーの意味を理解する - 動的 IP アドレスのリスクを説明する インストールと構成というセクション。全体の25%。 ActiveDirectoryというワードだけで不安になる。 Linux構成が選べるけどWindowsServerを選んだ方が良い気がしてきた。 内容的には妥当な感じ。 インストール - インストールの手順とオプションを理解する: - インストールパス - ゲートウェイポート - アイデンティティストアと SSO のオプションを理解する: - 外部 (Active Directory) とローカル - 信頼できるチケット - SAML - Kerberos と OpenID Connect - 自動ログインオプションの影響を説明する - SSL の設定方法を理解する - 単一マシン環境のインストールに関する Tableau のベストプラクティスを理解する - サイレントインストールを理解する Tableau Server の構成 - キャッシュ設定を理解する - 以下の方法を理解する: - プロセス分散の適用 - メールアラート/サブスクリプションの設定 - オプションで行えるカスタマイズの設定 - 以下を説明する: - サイト構成オプション - ユーザー数 - ストレージ容量 - サイトサブスクリプションの有効化および編集の方法 - プロジェクト構成オプション - グループとユーザーの構成オプション - 誰がユーザーを追加できるかを理解する ユーザーの追加 - ライセンスタイプとサイトロール - 管理者レベル - パブリッシャーレベル - Active Directory またはローカルを介したインポート セキュリティ - 以下のセキュリティ構成を説明する: - サイトレベル - プロジェクトレベル - グループレベル - ユーザーレベル - データソースレベル - ワークブックレベル パーミッション - 以下を理解する: - システムパーミッションの構成 - パーミッション設計の細部 - Tableau のセキュリティモデル - 許可、拒否、なしの違いを説明する 管理というセクション。全体の36%。 たぶんここが本丸だろう。 以下の方法を理解する: - データ接続を維持する - スケジュールを作成する - サブスクリプションを作成、編集、削除する - サーバー分析を実行する - バックアップと復元を完了する - クリーンアップを実行する - ユーザーを追加、削除、非アクティブ化する - ライセンスを更新する - 起動、停止、再起動する - tsm と tabcmd を使用する - REST API を使用する - ログファイルを処理する - 埋め込みを理解する - Desktop ライセンスの使用状況を監視する - ワークブックとデータソースのリビジョン履歴を管理する 以下の方法を説明する: - 複数の方法でサーバーのステータスを確認する - メールアラートを確認する - データドリブンアラートを設定する - 組み込まれた管理ビューを利用する - カスタム管理ビューを作成する - パフォーマンスの記録を作成する - ネストされたプロジェクトを作成する - サイトおよびサイト管理者のオプションを扱う エンドユーザーとシステム管理者の機能の比較 エンドユーザー機能 以下を理解する: - 表の推奨 - ビューとデータソースをパブリッシュする - ワークブックの名前を変更する - Web でビューを操作する - Web 作成と編集 - ビューの共有方法 - データソースの認証 - 抽出のキャッシング トラブルシューティングというセクション。全体の13%。 ブラウザでのサードパーティ Cookie の要件を理解する 以下の方法を理解する: - Tableau ユーザーまたは Tableau 実行サービスアカウントのパスワードをリセットする - レポーティング用のログファイルをパッケージ化する - tsm を使用してサイトのリソースを検証する - 検索インデックスを再構築する - メンテナンス分析レポートを使用する - サポートリクエストを作成する/開く 移行とアップグレードというセクション。全体の6%。 - アップグレードプロセスを理解する - クリーン再インストールを実行する方法と理由を説明する - 異なるハードウェアに移行する方法を説明する - 後方互換性を理解する おわり 正直どんな風に聞かれるのかがわからないことが一番やっかい. オンラインヘルプを参照可能なので暗記する必要はないのだが, 時間を考慮すると覚えておいてオンラインヘルプで確認するというフローが現実的ではないか. 上から順に流していくと気づくことがある. 実際にインストール,セットアップしようとすると知らなければいけないことが並んでいる. 使わないけど知識として並べてある、という他資格とは印象が違う. あまり潰しが効く技術ではないので,良くも悪くも使い方のガイドとして使うべきだろうと思う.

default eye-catch image.

MacでDockerお砂場を作る

docker on vagrant Docker for Macが遅いので,Webのコード書くのはvagrant+ansibleで済ませていたのだけれども, vagrant上にdockerを立てることでLinux上で走らせるのと同レベルの速度を得られるようなので, docker on vagrantを立ててdockerに入門してみる。 Web開発していない人から見ると,なんでdocker使わないの? ってことなのだが, 気持ちは以下の通り. 工夫無しだと1度試したら2度とやりたくないレベルで使い物にならない. Docker For Macが遅い:対策の実験 Macのdockerが遅いストレスから解放されよう ansibleで自動化してたから手間に気づかなかったけど, 公式がイメージ配ってるんだから,手間なしなのはdocker. 本番環境と開発環境を揃えにくいかなとも思うけど, AWS ECS的なものもあって,そもそもdockerだけで完結する世界が主流になりそうな感. docker on vagrantで遅さを回避できるので, 言い訳してないでアップデートしていく.. なぜ遅いのか Docker for Macは, AlpineLinuxベースのHyperkitVMの上で透過的にContainerを扱う. Mac上でDockerコマンドを実行すると,透過的にHyperkitVM上に反映される. Macの上で直接Containerが動いているのではなくこのVMの上でContainerを動かしている. HostとGuestのファイルシステムをマウントする観点ではvagrantも同じで, 実際,VagrantfileでマウントオプションとしてNFSを指定したとしてもNativeよりかなり遅い. ファイルシステムの対称性がある分,vagrantはHostとGuestをSyncする方法に手を入れやすく, HostとGuestのファイルシステムをNativeと同等レベルの速度でSyncする手段を導入できる. この仕組みによると, vagrantの上でDockerを動かすパフォーマンスがNative並に速くなる. Mutagen HostとGuestのファイルシステムをSyncするためにMutagenを利用する. Overviewによると,Mutagenは双方向の同期ツールで,低レイテンシをうたっている. もともとHostToGuestというよりはLocalToCloudの統合を目指している感じ. キーボードを打ってから反映されるまでのラグが気になる, とかOSが違う場合のアレコレ(permissionとかシンボリックリンックとか),とか, そういうところの解消を目指している様子. エージェントレス,TCP,でリモートへの導入コストが少ないのが良い. VagrantにはMutagen over SSHの形を取る. インストール手順 こちらが参考になりました。わかりやすく,手順通りやれば10分かかりません. Vagrantを使う「Mac最速のDocker環境」を初心者向けに解説【遅いMac for Dockerを卒業】 とりあえず箱だけ作った.Laravelだと分かりづらいのでRailsで試したところ激しく速かった. 副次的な効果として、Macを汚さないのでお砂場にぴったり.

default eye-catch image.

微分フィルタだけで時系列データの過渡応答終了を検知したい

ある値の近傍を取る状態から別の値の近傍を取る状態へ遷移する時系列データについて、 どちらの状態でもない過渡状態を経て状態が遷移するみたい。 今回知りたいのは理論ではなく、定常状態に遷移したことをいかにして検知するかという物理。 まぁいかようにでも検知できなくもないけれども、非定常状態の継続時間は不明であるという。 なるべく遅れなしに定常状態への遷移を検知したい。 ARモデルとかMAモデルとかの出番ではない。 DeepLearningとかやってる暇はないw 過去の微小時間内に含まれるデータだけから次が定常状態なのかを検知したい。 前状態と後状態のベースラインがどんな値であっても、 それを考慮することなしに過渡応答の後にくる定常状態の先頭を検知できる、という特徴がほしい。 移動平均だとベースラインがケースごとに変わるので扱いづらい。 微分であればベースラインがゼロに揃うのが良いな、と思った。 結局、今度はゼロへの収束を検知する問題に差し代わるだけだけども、 前ラインと後ラインの2つも見ないでも、確実に楽にはなっている。 微分するとノイズが増えるが...。 微分値が上下の閾値を超えたラインから先、 ゼロ収束の判定条件が一定時間継続した最初のデータポイントがソコ。 これは過渡状態の終了検知なだけだから、それとRawデータの変化をみる。