Kevin's Data Analytics Blog

データサイエンティスト、AIエンジニアを目指す方に向けて情報発信していきます。

二項分布が正規分布に近似する様子をアニメーションにしてみた|Processing

今回も前回に続き、Processingを使ってアニメーションを作成しました。今回は、Processingについて更に理解を深めるために、クラスを使ってみました。

アニメーションイメージ

ボールが50%の確率で左か右に振れながら落ちていく時、最終的な落下位置は二項分布に従うとみなせます。

また、試行回数が大きいとき、二項分布は、正規分布に近似できることが知られています。
今回、500個のボールを落とした時の様子をProcessingで実装し、その散らばりが正規分布の形になることを確認してみました。

youtube.com

アニメーションのコード

以下の点を意識して実装しました。

  • ボール用のクラスを作成して、位置・移動の方向・色などボールごとに異なる情報は、クラスのフィールドに持たせる
  • ボールに関する処理(移動、描画)をクラスのメソッドとして定義する
  • それ以外の要素は、グローバル変数として管理する

以下がコードになります。

// 二項分布が正規分布に近似する様子

// 変数宣言
int t = 0;  // 時間管理用変数
int ballnum = 500;  // 玉の数(初期値)
int ballsize = 11;  // 玉のサイズ(初期値)
float surface_rate = 0.65;  // 水面の位置
boolean reset_flag = false;  // リセットフラグ
int reset_count = 0;  // リセットタイミング調整用のカウンタ
Ball[] balls = new Ball[ballnum];  // 玉の配列

// 初期化 最初に1回だけ実行される
void setup() {
  // フレームレート(1秒ごとに表示されるフレーム数)を指定
  frameRate(10);
  
  // 画面サイズを指定
  //fullScreen(); // フルスクリーン用
  size(540, 980); // 縦長表示用
  
  // カラーモード指定
  colorMode( HSB, 360, 100, 100, 100 );
  
  // 初期化
  for (int i=0; i<balls.length; i++) {
    balls[i] = new Ball(width/2  // x座標
                          ,ballsize*3  // y座標
                          ,ballsize  // x軸方向への移動幅
                          ,ballsize+2  // y軸方向への移動幅
                          ,random(0, 360)  // 色相
                          ,random(50, 60)  // 彩度
                          ,random(90, 100));  // 明度
  }
}

// 図形を描画 ループして実行されるためアニメーションになる
void draw() {
  // 背景色
  background(0,0,100);
  
  // 水色の四角を描画
  noStroke();
  fill(180,100,100,10);
  rect(0, height*surface_rate, width, height);

  // 玉の描画と移動
  for (int i=0; i<balls.length; i++) {
    // 時間tの値に応じて、配列内の玉を1つずつ動かし始める
    if (t > i){
      balls[i].drawEllipse();
      balls[i].move();
    }
  }

  // 玉同士の接触判定
  for (int i=0; i<balls.length; i++) {
    for (int j=i+1; j<balls.length; j++) {
      
      // 落下の開始地点付近では判定はしない
      if ((balls[i].y <= ballsize*4) || (balls[j].y <= ballsize*4)) {continue;}
      
      // 玉の中心同士の距離 = √((x1-x2)^2+(y1-y2)^2)
      float dist = sqrt(sq(balls[i].x - balls[j].x) + sq(balls[i].y - balls[j].y));
      
      // 玉のサイズより近づいたら接触とみなして動きを止める
      if (dist < ballsize) {
        balls[j].speedX = 0;
        balls[j].speedY = 0;
        // 0.4個分ずらして積み上げる
        balls[j].y=balls[i].y - ballsize*0.4;
        continue;
      }
    }
  }

  // リセット判定
  int sum_speedY = 0;
  // 全ての玉の落下速度(sppedY)の和を計算
  for (int i=0; i<balls.length; i++) {
    sum_speedY += balls[i].speedY;
  }
  // 0になっていたらreset_flagをtrueにする
  if (sum_speedY == 0){
    reset_flag = true;
    reset_count++;
  }  
  
  // リセット処理  ※30フレーム分制止する
  if (reset_flag && reset_count > 30){
    
    // ボールの位置などを全て初期化
    for (int i=0; i<ballnum; i++) {
      balls[i].x = width/2;  // x座標
      balls[i].y = ballsize*3;  // y座標
      balls[i].speedX = ballsize;  // x軸方向への移動幅
      balls[i].speedY = ballsize+2;  // y軸方向への移動幅
      balls[i].col1 = random(0, 360);  // 色相
      balls[i].col2 = random(50, 60);  // 彩度
      balls[i].col3 = random(90, 100);  // 明度
    }
    reset_flag = false;
    reset_count = 0;
    t = 0;
  }
  
  // 動画作成用にpngファイルを保存 ※保存時にコメントインする
  //saveFrame("frames/######.png");
  
  // 時間を進める
  t+=1;
}

// クラスを定義
class Ball {
  // フィールド変数を宣言
  float x;  // x座標
  float y;  // y座標
  float speedX;  // x軸方向への移動幅
  float speedY;  // y軸方向への移動幅
  float col1;  // 色相
  float col2;  // 彩度
  float col3;  // 明度

  // constractorを初期化
  Ball(float _x, float _y, float _speedX, float _speedY,
        float _col1, float _col2, float _col3) {
    x = _x;
    y = _y;
    speedX = _speedX;
    speedY = _speedY;
    col1 = _col1;
    col2 = _col2;
    col3 = _col3;
  }

  // メソッド関数
  // 玉を移動
  void move() {
    // y軸方向に移動
    y += speedY;
    
    // x軸方向への移動は、水面より上では、ランダムで左右に動く
    if (y < height*surface_rate){
      if ((int)random(2)%2 == 0){
        x += speedX;
      }else{
        x -= speedX;      
      }
    }
    
    // 下端に到達したらy方向への移動を止める
    if (y > height-ballsize){
      speedY = 0;
      y = height-ballsize/2;
    }
  }
  
  // 玉を描画 
  void drawEllipse() {
    // 縁なし
    noStroke();
    
    // 玉の色を指定
    fill(col1, col2, col3, 100);
    
    // 位置とサイズを指定して、玉を表示
    ellipse(x, y, ballsize, ballsize);
  }
}

まとめ

クラスを使うことで、コードが構造化できた気がします。
今回は、200行弱のコードとなりましたが、コードの読みやすさと備忘のために改行やコメントを多めに入れている分を踏まえると、実際は100行程度で書けると思います。また私自身がProcessing初心者のため、冗長な部分もあると思います。もし、バグなどにお気づきの方は、ご指摘いただけると嬉しいです。
もう少しProcessingを使って色々描いてみて、スキルアップしたいと思います。

最後まで読んでいただきありがとうございました!