20
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

1. はじめに

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

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

2. 重み付きランダム関数 weightedRandom の実装

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

/**
 * 指定されたアイテムの配列から、重み(確率)に基づいてランダムに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. プロジェクトのディレクトリを作成
  2. package.json を作成
  3. 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 でシードデータを作成する際に活用しました。

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

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

20
12
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?