roombaの日記

読書・非線形科学・プログラミング・アート・etc...

今話題の「異人種住み分け」をシミュレーションにしてみる

はじめに

最近、移民に関連して「居住区は人種ごとに分けたほうが良い」というような発言が話題となっています。
曽野綾子さん「移民を受け入れ、人種で分けて居住させるべき」産経新聞で主張

その是非は誰かに考えてもらうとして、これをきっかけに人種ごとの住み分けに関する興味深いモデルを思い出したのでシミュレーションを行ってみることにしました。この記事上で自由に動かすことができます!


シェリングの「分居モデル」

分居モデルとは?

上述の「興味深いモデル」とは、アメリカのトーマス・シェリング氏が1971年に考案した「分居モデル」です。Wikipediaによれば、このモデルには以下のような意義があります。

たとえ白人と黒人が隣同士で暮らすことに抵抗がなくとも、いつの間にか白人が多く居住する地域と黒人が多く居住する地域に分かれてしまう理由をゲーム理論(マルチエージェントシミュレーション,Multi-Agent Simulation,MAS)的に説明した。

トーマス・シェリング - Wikipedia

どうやるの?

彼のモデルでは、8×8のマス目上に人種Aが23人・人種Bが22人いるとします。最初はランダムにこの45人がいるのですが、個々の居住者は自分の周囲に異人種が多すぎる場合に限り引越しをします。より具体的には、周囲8つのマス目にいる人間のうち、同じ人種が1/3に満たない場合に限り引越しを行い、それ以外の場合には満足してその場にとどまります。みんなが満足するまでこれを繰り返します。

結果は?

これを実行するとどうなるでしょうか? 周囲の2/3近くが異人種でも満足するという「寛容な」人々を想定しているのですから、直感的には住み分けは生じにくそうです。

しかし結果は異なります。個々の人間は寛容でも、社会全体を眺めてみると分居が進むのです。これを明らかにした点がシェリングのモデルのすごい所です。

ほんまかいな、という感じなので、次章でシミュレーションを行います。

シミュレーション

シェリングの時には今のようにコンピュータで簡単にシミュレーションができなかったため、彼はチェッカーボード上で1セント硬貨・10セント硬貨(それぞれ異なる人種の人間に対応)を用いて研究を行いました。
しかし我々は現代人なので、HTML5 Canvas, Javascriptを用いてシミュレーションをブラウザ上で実行できるようにします。ブログ上でHTML5 Canvasを利用する方法は以前の記事で紹介しました。

HTML5 Canvasで動くジェネラティブ・アートをブログに貼る - roombaの日記

つくったシミュレーションをすぐ下に貼り付けました。PCやスマホのブラウザ上で実際に動かしてみることができます。

使い方・見方

  • 最初は灰色の人と茶色の人がばらばらに混在している
  • 青色のStartボタンをクリック(タップ)するとシミュレーションが始まる
  • シミュレーションの進行につれて人種ごとに住み分ける様子が観察できる
  • 自動的に止まる
  • 緑色のResetボタンをクリック(タップ)すると新たな初期配置になり、もう一度青色のStartボタンをクリック(タップ)することでシミュレーションを開始できる

↓↓↓ 実際に試してみてください。


step


ソースコードは長いので記事末尾に添付しました。

おわりに

このように、個々の人間に差別意識があるわけでは無くて若干同人種のほうが快適かなーぐらいでも、全体として見ると住み分けが進んでしまうという示唆が得られました。
もちろん、シミュレーション(特に社会シミュレーション)を見る際には「何を考慮にいれて何を無視するか」といった前提・モデル化を常に疑ってかかる必要がありますが。

本記事で作成したシミュレーションは、下記の書籍を参考にしました。
こちらの書籍ではartisocというマルチエージェントシミュレーション用ソフトに特有の言語で記述されていますが、同様のことをJavascriptで実装しました。余裕だと思っていたのですが、1から自分でつくると地味に大変ですね…

人工社会構築指南(シリーズ人工社会の可能性1)

人工社会構築指南(シリーズ人工社会の可能性1)

汚いソースコードはこちら↓
はてなブログならこれを執筆画面にコピペするだけで動くようになります。

<button onclick="start()" style="font-size: 1em; font-weight: bold; padding: 0px 20px; background-color: #248; color: #fff; border-style: none;"> 
    Start
</button>
<button onclick="reset()" style="font-size: 1em; font-weight: bold; padding: 0px 20px; background-color: #284; color: #fff; border-style: none;"> 
    Reset
</button>
<canvas id="a_canvas" width="300" height="300" style="padding: 0; margin: 0; border: 0;"></canvas>
<div id="step"></div>
<script type="text/javascript">
var canvas = document.getElementById("a_canvas");
var ctx = canvas.getContext("2d");
canvas.width = 300;
canvas.height = 300;

// global variables
var board_size = 8;// 盤面はboard_size x board_size
var num_penny = 23;// Number of penny agent
var num_dime = 22;// Number of dime agent
var penny_pos = new Array();// 各pennyの位置(x, y)の配列
var dime_pos = new Array();// 各pennyの位置(x, y)の配列
var board = new Array();// 盤面にpenny or dimeが存在するかの2次元配列 trueなら存在
var rate_threshold = 1.0 / 3;// 周囲の仲間がこれ以下なら移動
var step = 0;// 経過ステップ
var move_count = 0;// あるステップに移動した数 これが0になると終了

////////////////////////////////
// ---------- 実行 ---------- //
////////////////////////////////

init();// 初期化
draw_agt();
draw_board();
// startボタンが押されたら開始
function start(){
    timer = setInterval(update, 300);// update()関数を定期的に実行
}
// resetボタンが押されたらリセット
function reset(){
    step=0;
    init();
    ctx.fillStyle = "rgb(255, 255, 255)";
    ctx.fillRect(0, 0, 300, 300);// 以前の画面を消す
    draw_agt();
    draw_board();
}

///////////////////////////////////
// ---------- 主な関数 ---------- //
///////////////////////////////////

// 各種初期化の関数
function init(){
    var i, j;
    // 2次元配列boardの初期化
    for (i=0; i<board_size; i++){
        board[i] = new Array();
        for (j=0; j<board_size; j++){
            board[i][j] = false;
        }
    }
    // ランダムにpenny, dimeを配置
    for (i=0; i<num_penny; i++){
        penny_pos[i] = new Array();
        // 座標を0 - board_size-1の間のランダムな整数に。かぶらないように。
        do {
            penny_pos[i][0] = Math.floor(board_size*Math.random());
            penny_pos[i][1] = Math.floor(board_size*Math.random());            
        }while (board[penny_pos[i][0]][penny_pos[i][1]] === true);
        // boardを更新
        board[penny_pos[i][0]][penny_pos[i][1]] = true;
    }
    for (i=0; i<num_dime; i++){
        dime_pos[i] = new Array();
        // 座標を0 - board_size-1の間のランダムな整数に。かぶらないように。
        do {
            dime_pos[i][0] = Math.floor(board_size*Math.random());
            dime_pos[i][1] = Math.floor(board_size*Math.random());            
        }while (board[dime_pos[i][0]][dime_pos[i][1]] === true);
        // boardを更新
        board[dime_pos[i][0]][dime_pos[i][1]] = true;
    }
}

// 1ステップ進める関数
function update(){
    var i;
    var np, nd, rate;
    move_count = 0;
    // penny
    for (i=0; i<num_penny; i++){
        // 周囲のdimeとpennyの数・仲間の割合を計算
        np = count_penny(penny_pos[i][0], penny_pos[i][1]);
        nd = count_dime(penny_pos[i][0], penny_pos[i][1]);
        if (np+nd === 0){
            rate = 0;
        }else{
            rate = 1.0 * np / (np + nd);
        }
        // 仲間の割合がしきい値以下なら移動
        if (rate < rate_threshold){
            move_penny(i);
        }
    }
    // dime
    for (i=0; i<num_dime; i++){
        // 周囲のdimeとpennyの数・仲間の割合を計算
        np = count_penny(dime_pos[i][0], dime_pos[i][1]);
        nd = count_dime(dime_pos[i][0], dime_pos[i][1]);
        if (np+nd === 0){
            rate = 0;
        }else{
            rate = 1.0 * nd / (np + nd);
        }
        // 仲間の割合がしきい値以下なら移動
        if (rate < rate_threshold){
            move_dime(i);
        }
    }
    // 再描画
    ctx.fillStyle = "rgb(255, 255, 255)";
    ctx.fillRect(0, 0, 300, 300);// 以前の画面を消す
    draw_agt();
    draw_board();
    document.getElementById('step').innerHTML = 'step=' + step;
    step++;// ステップ加算
    console.log(step);
    // 変化が無くなるか50stepで終了
    if ( (move_count == 0) || (step==50)){
        clearInterval(timer);
    }
}

//////////////////////////////////////////
// ---------- 途中で用いる関数 ---------- //
//////////////////////////////////////////

// x, y周りのpennyの数を計算する関数
function count_penny(x, y){
    var i;
    var ret = 0;
    for (i=0; i<num_penny; i++){
        // (x, y)の8近傍かどうかチェック
        if ( (Math.abs(penny_pos[i][0]-x)<=1) && (Math.abs(penny_pos[i][1]-y)<=1) && 
             (Math.abs(penny_pos[i][0]-x)+Math.abs(penny_pos[i][1]-y)>0) ){
            ret += 1;
        }
    }
    return ret;
}
// x, y周りのdimeの数を計算する関数
function count_dime(x, y){
    var i;
    var ret = 0;
    for (i=0; i<num_dime; i++){
        // (x, y)の8近傍かどうかチェック
        if ( (Math.abs(dime_pos[i][0]-x)<=1) && (Math.abs(dime_pos[i][1]-y)<=1) && 
             (Math.abs(dime_pos[i][0]-x)+Math.abs(dime_pos[i][1]-y)>0) ){
            ret += 1;
        }
    }
    return ret;
}

// penny_pos[index]の位置を周囲(視野2の範囲)に変更(移動)
function move_penny(index){
    var i, j;
    var curr_x = penny_pos[index][0];// current x
    var curr_y = penny_pos[index][1];
    var tmp_array1 = new Array();// 空白セルを一時的に保持
    var tmp_array2 = new Array();// 空白セルを一時的に保持
    var delta_x, delta_y, sel;
    // 視野2における空白セルをさがす
    for (i=-2; i<3; i++){
        for (j=-2; j<3; j++){
            // (i, j)ずれても範囲内の場合のみ扱う
            if ( (curr_x+i >= 0) && (curr_x+i < board_size) && 
                 (curr_y+j >= 0) && (curr_y+j < board_size) ){
                if (board[curr_x+i][curr_y+j]===false){//空白
                    tmp_array1.push(i);
                    tmp_array2.push(j);
                }
            }
        }
    }
    // 空白があればランダムに移動 なければそのまま
    if (tmp_array1.length > 0){
        // ランダムに選ぶ
        sel = Math.floor(tmp_array1.length*Math.random());
        delta_x = tmp_array1[sel];
        delta_y = tmp_array2[sel];
        // 移動
        penny_pos[index][0] = curr_x + delta_x;
        penny_pos[index][1] = curr_y + delta_y;
        // boardを更新
        board[curr_x][curr_y] = false;
        board[penny_pos[index][0]][penny_pos[index][1]] = true;
        // カウント
        move_count++;
    }
}

// dime_pos[index]の位置を周囲(視野2の範囲)に変更
function move_dime(index){
    var i, j;
    var curr_x = dime_pos[index][0];// current x
    var curr_y = dime_pos[index][1];
    var tmp_array1 = new Array();// 空白セルを一時的に保持
    var tmp_array2 = new Array();// 空白セルを一時的に保持
    var delta_x, delta_y, sel;
    // 視野2における空白セルをさがす
    for (i=-2; i<3; i++){
        for (j=-2; j<3; j++){
            // (i, j)ずれても範囲内の場合のみ扱う
            if ( (curr_x+i >= 0) && (curr_x+i < board_size) && 
                 (curr_y+j >= 0) && (curr_y+j < board_size) ){
                if (board[curr_x+i][curr_y+j]===false){//空白
                    tmp_array1.push(i);
                    tmp_array2.push(j);
                }
            }
        }
    }
    // 空白があればランダムに移動 なければそのまま
    if (tmp_array1.length > 0){
        // ランダムに選ぶ
        sel = Math.floor(tmp_array1.length*Math.random());
        delta_x = tmp_array1[sel];
        delta_y = tmp_array2[sel];
        // 移動
        dime_pos[index][0] = curr_x + delta_x;
        dime_pos[index][1] = curr_y + delta_y;
        // boardを更新
        board[curr_x][curr_y] = false;
        board[dime_pos[index][0]][dime_pos[index][1]] = true;
        // カウント
        move_count++;
    }
}



///////////////////////////////////////////
// ---------- 描画用の各種関数 ---------- ///
///////////////////////////////////////////

// (x1, y1)から(x2, y2)に太さwの線を引く関数
function draw_line(x1, y1, x2, y2, w){
    ctx.lineWidth = w;
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.closePath();
    ctx.stroke();
}

// board_size x board_size のマス目を書く関数
function draw_board(){
    var i;
    for (i=0; i<board_size+1; i++){
        // 縦線
        draw_line(i*(1.0*canvas.width/board_size), 0,
                  i*(1.0*canvas.width/board_size), canvas.height, 0.5);
        // 横線
        draw_line(0, i*(1.0*canvas.height/board_size),
                  canvas.width, i*(1.0*canvas.height/board_size), 0.5);
    }
}

// 左からx番目, 上からy番目(x,y:0から)のマス目に円を描く関数
function draw_circle(x, y){
    ctx.beginPath();
    ctx.arc((x+0.5)*(1.0*canvas.width/board_size), (y+0.5)*(1.0*canvas.width/board_size),
            0.5*canvas.width/board_size, 0, 2*Math.PI, true);//(x, y, r, theta_from, theta_to)
    ctx.fill();
}

// dimeとpennyのagentをそれらの位置に応じて描く関数
function draw_agt(){
    var i;
    for (i=0; i<num_penny; i++){
        ctx.fillStyle = "rgb(196, 112, 34)";
        draw_circle(penny_pos[i][0], penny_pos[i][1]);
    }
    for (i=0; i<num_dime; i++){
        ctx.fillStyle = "rgb(150, 150, 180)";
        draw_circle(dime_pos[i][0], dime_pos[i][1]);
    }
}
</script>