重み付きランダム選択関数の作り方 (TypeScriptで実装)

## 1. はじめに
JavaScript の Math.random() を使ったランダム選択は、すべての選択肢が等確率になりがちです。しかし、特定の要素を高確率で選ばせたい場合には 「重み付きランダム（Weighted Random）」 を使うと便利です。

本記事では、TypeScript で 重み付きランダム関数 を実装し、実際にコードを動かす方法を解説します。

## 2. 重み付きランダム関数 weightedRandom の実装
以下の weightedRandom 関数を実装することで、配列内のアイテムを 指定した確率（重み） に基づいてランダムに選択できます。

```ts
/**
 * 指定されたアイテムの配列から、重み（確率）に基づいてランダムに1つ選ぶ関数
 * @param {T[]} items - ランダムに選択したいアイテムの配列
 * @param {number[]} [weights] - 各アイテムの重み（確率）を指定する配列（省略可能）
 * @returns {T} - 選択されたアイテム
 */
function weightedRandom<T>(items: T[], weights?: number[]): T {
  const TOTAL_PERCENTAGE = 100; // 全体の重みの基準値（必ずしも100である必要はない）
  const DEFAULT_WEIGHT = 1; // デフォルトの重み（未指定の場合の重み）
  const MIN_WEIGHT = 0; // 最小の重み（負の値は許容しない）

  // weights が未指定または空なら、すべてのアイテムの重みを均等に設定
  if (!weights || weights.length === 0) {
    weights = Array(items.length).fill(DEFAULT_WEIGHT);
  }

  // 指定された重みの合計を計算
  const totalSpecifiedWeight = weights.reduce((sum, weight) => sum + weight, MIN_WEIGHT);

  // 指定されていないアイテムの数を計算
  const remainingItemsCount = items.length - weights.length;

  // 残りのアイテムに割り当てる重みを計算
  const remainingWeight = Math.max(TOTAL_PERCENTAGE - totalSpecifiedWeight, MIN_WEIGHT);

  // 残りのアイテムがある場合、それぞれに均等な重みを割り当てる
  const remainingItemWeight = remainingItemsCount > 0 ? remainingWeight / remainingItemsCount : MIN_WEIGHT;

  // 重みのリストを調整（指定されたもの + 自動計算されたもの）
  const adjustedWeights = [
    ...weights,
    ...Array(remainingItemsCount).fill(remainingItemWeight),
  ];

  // 調整された重みの合計を再計算
  const totalWeight = adjustedWeights.reduce((sum, weight) => sum + weight, MIN_WEIGHT);

  // 0 以上 totalWeight 未満のランダムな値を生成
  let random = Math.random() * totalWeight;

  // 各アイテムの重みを順番に引いていき、ランダム値がその範囲にあるアイテムを選択
  for (let i = 0; i < items.length; i++) {
    if (random < adjustedWeights[i]) {
      return items[i];
    }
    random -= adjustedWeights[i];
  }

  // 万が一すべての条件を通過してしまった場合、最後のアイテムを返す
  return items[items.length - 1];
}
```

## 3. 関数の動作例
以下のように weightedRandom 関数を呼び出すことで、指定した確率に基づいてランダムな値を取得できます。

## 4. TypeScript 環境をセットアップ
1. プロジェクトのディレクトリを作成
1. package.json を作成
1. TypeScript の開発環境を整えるために、必要なパッケージをインストール
```
mkdir ts-practice 
cd ts-practice
npm init -y
npm install --save-dev typescript ts-node @types/node

code . # vscodeを立ち上げる
```

4.TypeScript の設定ファイルを作成
```
npx tsc --init
```

5.スクリプトを追加
package.json の "scripts" セクションに、以下のスクリプトを追加します。
```
"scripts": {
  "start": "ts-node index.ts"
},
```

## 6. TypeScript コードをターミナルで実行

```
touch index.ts
```

```
echo '/**
 * 指定された重みに基づいて、ランダムに `items` の中から 1 つを選択する関数
 *
 * @param items 選択対象のアイテムの配列（例: `['A', 'B', 'C', 'D']`）
 * @param weights 各アイテムの選択確率を示す数値の配列（例: `[60, 30]`）
 *    - `weights` を指定しない場合、すべての `items` が均等確率で選ばれる
 *    - `weights` の合計が 100% 未満の場合、残りの確率は未指定のアイテムに均等配分される
 * @returns ランダムに選ばれた `items` の要素
 *
 * @example
 * A(60%)、B(30%)、C, D(5%ずつ)
 * weightedRandom(['A', 'B', 'C', 'D'], [60, 30]);
 *
 * A(40%)、B(30%)、C(20%)、D(10%)
 * weightedRandom(['A', 'B', 'C', 'D'], [40, 30, 20, 10]);
 *
 * A, B, C, D の確率が均等 (25%ずつ)
 * weightedRandom(['A', 'B', 'C', 'D']);
 */

function weightedRandom<T>(items: T[], weights?: number[]): T {
  const TOTAL_PERCENTAGE = 100;
  const DEFAULT_WEIGHT = 1;
  const MIN_WEIGHT = 0;

  if (!weights || weights.length === 0) {
    // weights が未指定または空なら、全アイテムを均等にする
    weights = Array(items.length).fill(DEFAULT_WEIGHT);
  }

  const totalSpecifiedWeight = weights.reduce(
    (sum, weight) => sum + weight,
    MIN_WEIGHT,
  );
  const remainingItemsCount = items.length - weights.length;

  // 残りの割合を計算（TOTAL_PERCENTAGE から既存の合計を引く）
  const remainingWeight = Math.max(
    TOTAL_PERCENTAGE - totalSpecifiedWeight,
    MIN_WEIGHT,
  );

  // 残りのアイテムがある場合、それぞれに均等に配分
  const remainingItemWeight =
    remainingItemsCount > 0
      ? remainingWeight / remainingItemsCount
      : MIN_WEIGHT;

  const adjustedWeights = [
    ...weights,
    ...Array(remainingItemsCount).fill(remainingItemWeight),
  ];
  const totalWeight = adjustedWeights.reduce(
    (sum, weight) => sum + weight,
    MIN_WEIGHT,
  );

  let random = Math.random() * totalWeight;

  // random が現在の adjustedWeights[i] 未満なら、そのアイテムを返す（選択される）。
  // そうでない場合、random から adjustedWeights[i] を引いて次の候補へ。
  for (let i = 0; i < items.length; i++) {
    if (random < adjustedWeights[i]) {
      return items[i];
    }
    random -= adjustedWeights[i];
  }

  return items[items.length - 1]; // 理論上ほぼ起こらないが、念のため最後の要素を返す
}

// true (70%) / false (30%)
console.log(weightedRandom([true, false], [70, 30]));

// true (90%) / false (10%)
console.log(weightedRandom([true, false], [90, 10]));

// true/false が均等確率 (50%ずつ)
console.log(weightedRandom([true, false]));

enum PostStatus {
  Publish = "Publish",
  Draft = "Draft",
}

// 公開 (80%) / 下書き (20%)
console.log(weightedRandom([PostStatus.Publish, PostStatus.Draft], [80, 20]));

// 均等確率で選択
console.log(weightedRandom([PostStatus.Publish, PostStatus.Draft]));

console.log(weightedRandom([10, 20, 30, 40, 50], [50, 20, 10])); 
// 10(50%), 20(20%), 30(10%), 40(5%), 50(5%)

console.log(weightedRandom(["Apple", "Banana", "Cherry", "Date"]));
// 全て均等

const loot = ["Common", "Rare", "Epic", "Legendary"];
console.log(weightedRandom(loot, [60, 30, 9, 1])); 

enum UserRole {
  Admin = "Admin",
  User = "User",
  Guest = "Guest",
}

// Admin(10%), User(80%), Guest(10%)
console.log(weightedRandom([UserRole.Admin, UserRole.User, UserRole.Guest], [10, 80, 10]));

' > index.ts
```

実行
```
npm start
```

## 7. まとめ
本記事では、TypeScript で 重み付きランダム関数 を実装し、実際に環境をセットアップして動かす方法を解説しました。

今回学んだポイント
✅ 重み付きランダムとは？
✅ TypeScript での実装方法
✅ 開発環境のセットアップ
✅ 実際に動作させる方法

この weightedRandom 関数を活用すれば、確率に基づいたランダムな選択を行うことができ、シードデータの作成 などに役立てることができます。
実際に、私も NestJS でシードデータを作成する際に活用しました。

例えば、ユーザーのロール（管理者・一般ユーザーなど）や、ランダムなデータのバリエーションを調整するのに便利です。確率をコントロールしながらデータを生成できるので、テストデータをより実用的なものにすることができます。

ぜひ、シードデータの作成やその他の用途に活用してみてください！ 🚀