PowerAutomateで、ReservoirSamplingを実装してみた。

こんにちは、 @tariki-code こと[コバッチ](https://twitter.com/kobatch_tk)です。
前回の記事の続編で、PowerAutomateでReservoirSamplingを実装してみる事をやってみました。

## きっかけ

2023 PowerAutomate AdventCalenderを投稿させていただきました。

https://qiita.com/advent-calendar/2023/powerautomate

18日目の記事です。背景となるフローの詳細もこちらに記載してますので、もしよろしければご参照ください。

https://qiita.com/tariki-code/items/e02e24e6c41c9f6c249a

やりたいことは、Teams上のとあるチームに参加しているメンバー（200名程度）から、アイスブレイクな質問を送るべくランダムに抽出したい・・・というもので、そのPowerAutomteでの実装方法について記事を書かせていただきました。

この時に、以下のようなコメントをいただいたので、少し調べてやってみようと思ったのがきっかけです！！（@igrep様、ありがとうございます。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/ab2de2e5-37b4-e9d2-87b6-1dc220192b3d.png)


## ReservoirSamplingとは？

* 大容量のデータストリームや配列からランダムに要素を取得するためのアルゴリズム。
* 要素の数が不明でも、大量の領域を使ったり、1件づつ舐めるような事をせずとも、均一な確率でランダムにデータを取得したい時に使われる。

きちんとした参考文献を見ていただければとは思いますが、一旦GPT君に相談した時に受けた説明が以下です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/f2ab457f-89db-974f-b5c1-bc2e09976037.png" width=700>

## JavaScriptでアルゴリズムを書いてみる

書いてみると言いますか、面倒なのでGPT君に頼んで書いてもらいました。笑
コードは以下です、一応Githubにも挙げてみました。

https://github.com/takafumikobayashi/reservoir-sampling/blob/main/index.js

```javascript:index.js
function reservoirSampling(stream, k) {
    // Initialize the reservoir with the first k elements
    let reservoir = [];
    for (let i = 0; i < k; i++) {
        reservoir[i] = stream[i];
    }

    // Process the remaining elements in the stream
    for (let i = k; i < stream.length; i++) {
        // Randomly decide whether to replace an element in the reservoir
        let j = Math.floor(Math.random() * (i + 1));
        if (j < k) {
            reservoir[j] = stream[i];
        }
    }

    return reservoir;
}

// Example usage
const dataStream = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50];
const k = 5; // Number of samples to select
const sampledData = reservoirSampling(dataStream, k);
console.log("Sampled Data:", sampledData);
```

`dataStream`が対象となる配列で、サンプルでは1から50までの数字が入っています。
`k`がサンプリングしたい数です。
これら２つのパラメータを渡して`reservoirSampling`の関数を実行します。

結果はこんな感じです。

```bash:terminal
bash-3.2$ node index.js
Sampled Data: [ 20, 2, 18, 4, 13 ]

bash-3.2$ node index.js
Sampled Data: [ 11, 45, 3, 16, 27 ]
```

・・・と毎回違う結果が返ってきます。

関数内の`let j = Math.floor(Math.random() * (i + 1));`が処理の中核部分でこちらは、２つのステップの処理を行っています。

ランダムな数の生成：

* `Math.random()` は、`0`以上`1`未満のランダムな浮動小数点数を生成します。
* このランダムな数に `(i + 1)` を掛けることで、`0`以上 `i` 以下の範囲のランダムな浮動小数点数が得られます。ここでの`i`は現在処理しているデータセット内の要素のインデックスです。

整数への丸め：

* `Math.floor()`関数は、与えられた数を下方向へ最も近い整数に丸めます。
*  結果として、`0`から`i`の範囲内でランダムな整数`j`が生成されます。

この生成されたランダムな整数`j`は、`reservoir`内の要素のどれかを現在の要素（`i`番目の要素）と置き換えるかどうかを決定するために使われます。もし`j`が`reservoir`のサイズ`k`未満の場合、`reservoir`内の`j`番目の要素が現在の要素で置き換えられます。これにより、データセットの各要素が最終的なサンプルに含まれる確率が等しくなるように設計されています。

## PowerAutomateでどう実装する？

PowerAutomateで実装するなら、Excel Onlineを使って、OfficeScriptを実行させるのが一番楽かなと思いました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/89a76187-7947-4803-f60c-24f4fd6a8cca.png" width=700>

想定しているフローのイメージは以下の通りです。
こう見ると、`rand`関数をちょっと豪華にした処理に過ぎないようにも見えます。。笑
まぁでもやってみたいと思います。

```mermaid
flowchart TB

X[start]
A[グループメンバーの一覧表示]
B[1. グループメンバーの一覧表示で取得できた件数を取得]
C[2. グループメンバーからサンプリングで取得したい数を決める]
D[1, 2をパラメータにOfficeScriptでResevoirSampling処理実行]
E[/ループ開始 - ResevoirSampling処理で取得した配列の数分\]
F[グループメンバーの一覧表示からランダム取得した数をアドレスにもつ要素のUPNを取得]
G[\ループ終了/]
Z[End]

X --> A
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G -->|次の値あり| E
G -->|配列の最後の値まで処理| Z

```

## Excelでコード書いてみる

Office365からExcelOnlineで任意のファイルを立ち上げ、「Automate」のメニューから「NewScript」をクリックすると、エディターが起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/4a1dc96b-63ef-a561-1d34-409f68a1a6ff.png)

OfficeScriptはTypeScriptがベースになっているようです。先ほどのGPT君に作ってもらったサンプルソースコードの型指定の部分など若干修正します。ちなみに数値配列の変数を指定するのであれば`foo: number[]`と書きます。

また、処理をシンプルにするために、要素数とサンプリングをしたい数を指定したら、その条件でその要素を取得する際アドレスとなる数字配列を返すという処理にしました。

実装したソースコードは以下となります。

```typescript:Reservoir Sampling.xlsx
function main(workbook: ExcelScript.Workbook, length: number, k: number){
    
  // Initialize Stream
  let stream: number[] = [];
  for (let i = 0; i < length; i++) {
    stream[i] = i
  }

  // Initialize the reservoir with the first k elements
  let reservoir: number[] = [];
  for (let i = 0; i < k; i++) {
    reservoir[i] = stream[i];
  }

  // Process the remaining elements in the stream
  for (let i = k; i < stream.length; i++) {
    // Randomly decide whether to replace an element in the reservoir
    let j = Math.floor(Math.random() * (i + 1));
    if (j < k) {
      reservoir[j] = stream[i];
    }
  }
  console.log("reservoir:", reservoir);
  return reservoir;
}
```

`length`で要素数を受け取ります。受け取った要素数のある数値配列を`stream`として作成します。
`k`はサンプリングしたい数です。

## 実行確認をしてみる

「Run」をクリックすると実行確認ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/9ccb8813-cd83-1a6c-a0ce-312ab776fd33.png)

パラメータが設定されている場合は、入力が求められます。
この例だと、100要素の中から、10個のサンプルを取ってきてくださいという意味になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/9a786962-45f2-d3c9-8154-e3cd40ebb659.png)

実行結果はこうなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/c588e662-ce62-d9a9-5a46-1e14d84c64e0.png)

## PowerAutomateから呼び出してみる

ちょっと雑かもしれませんが。。w、とりあえず作ってみました、
全体のフローです。

### 前半

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/fb4d9ff4-351c-2186-d526-a5eb9320937d.png" width=500>

### 後半

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/36a39b62-4a35-153f-c721-6b5f2433a15f.png" width=500>


### グループメンバー一覧表示

単純にチームのメンバー一覧を取得してきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/7500d574-c87b-f163-7d5a-0ecc693e2f87.png)

### 変数を初期化する１（lengthの設定）

取得してきたグループメンバーのlengthを取ります。これも簡単ですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/b635417c-16e5-8316-62bc-639eff013c27.png)


```javascript:変数を処理化する１
length(outputs('グループ_メンバーの一覧表示')?['body/value'])
```

### 変数を初期化する２（kの設定）

常に10%のサンプリング率になるように`k`の計算を行います。
単純に`length`に10%を乗算した結果を整数に切り上げるという処理です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/442a79dc-0290-a647-a4a1-c5dc045048c9.png)

```javascript:変数を初期化する２
int(formatNumber(mul(variables('length'), 0.1), 'F0'))
```

少ないチームでも最低でも5名選出はしようということで、条件を追加してみました。
`k`の計算結果が、5未満でかつ`length`が10より大きい場合だけ、強制的に`k=5`としてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/fbf60076-93b3-bed8-ff7a-6420db4c7756.png)

### スクリプトの実行

ExcelOnlineで「スクリプトの実行」アクションを選択すると、実行するスクリプトファイルが保存されているファイルの選択ができます。

また、そのスクリプトのパラメータの入力が求められるので指定をします。今回は`length`と`k`が必要なので、それぞれフローの中で設定した変数を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/3329f12c-1e9b-bbde-33e6-dd9f4192753c.png)

### 変数を初期化する（スクリプト実行結果の取得）

実行したスクリプトの結果の配列を受け取ります。
そのまま配列変数に突っ込んでいるだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/12a7f2bb-afe2-9f96-df0b-e728b3f250cc.png)

### Apply to Each

配列の数分だけループを回します。
グループメンバー一覧で取得してきた要素数の範囲でランダムな数字を持っているので、そのアドレスを指定する形で値を取得します。

今回はUPNだけで良いので、要素を指定して取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/c4dc1a27-ec45-956f-d23a-b6ba4e9547d2.png)

```javascript:配列を変数に追加
outputs('グループ_メンバーの一覧表示')?['body/value'][items('Apply_to_each')]?['userPrincipalName']
```

## 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/35ddc756-373f-c463-8901-c1d09c596507.png)

スクリプト実行によって、うまくランダムな数字を取得できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/76811a7b-f083-2647-cd3b-38c09f8ce84f.png)

また、取得したランダム数字を使って、そのアドレスを持つユーザーのUPNも問題なく取得できています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/75448c56-65f4-69ae-4df7-daf38269e5f1.png)

これによって、前記事の処理ではグループメンバー一覧の要素数だけApply to Eachする必要が無くなったので処理速度としては改善できることが期待できます。

## 感想

* 毎回思うことですが、もっといいやり方を知っている人は是非コメントなどで教えてくださいー
* 今回はとりあえずサンプル的にフロート作ってみましたが、実際のフローに適用してみて、処理速度の面についてはもう少し検証してみたい。
OfficeScriptはプログラミングをやってきた人にとっては他のケースでもうまく活用できるかも、TypeScriptがベースになっているのでかなり個人的にはとっつきやすいし、フローとコード部分の棲み分けもキレイな疎結合で管理できそうな気がしました。
* 2024/01/20開催、「気ままに勉強会#75」でLTさせていただきますー。

https://kimamani.connpass.com/

## ReservoirSamplingに関する参考URL

参考にさせていただきました、ありがとうございます。

https://qiita.com/otoiku/items/ab994263f082675a806b

https://en.wikipedia.org/wiki/Reservoir_sampling