Juliaで座席選択の人間心理をシミュレーションしてみた
はじめに
通勤電車に乗っていると、空席がいくつかあっても「なぜそこに座らないの?」と思うことがあります。誰かの隣が避けられていたり、端の席から埋まっていったり。実はこれ、無意識のうちに人の心理が働いているのです。
この記事では、そんな「座席選択の心理」をシミュレーションするための数理モデルを、Juliaで構築してみました。
モデルの考え方
電車の座席選びには、いくつかの心理的な傾向が見られます。以下のような要素をモデルに取り入れました:
- 他人と距離を取りたい(端の席が人気)
- 女子高生の隣は座りにくい(気まずさ)
- 大柄な人の隣は狭そうで避けたい(快適さ)
- 誰も座っていない席はちょっと怖い(観察学習)
これらを表現するために、乗客には以下のような性格パラメータを持たせます。
乗客の性格パラメータ
| パラメータ | 説明 |
|---|---|
| sociability | 社交性:他人の隣でも気にしない度合い |
| sensitivity | 配慮性:気まずさや狭さへの敏感さ |
| observational | 観察学習:他人の行動を参考にする傾向 |
座席の設定
- 横1列に10席(Seat 1 〜 Seat 10)
- Seat 3 に女子高生、Seat 7 に大柄な男性が座っている(固定)
- 他の席は空席。1人ずつ順番に乗車して、どこに座るかを選びます。
実装(Julia)
ランダムに生成した乗客が、性格パラメータに応じて各席の「スコア」を計算し、softmax関数を使って着席先を選びます。
softmax関数(Julia)
function softmax(x::Vector{Float64}; beta::Float64=1.0) exps = exp.((x .- maximum(x)) .* beta) return exps ./ sum(exps) end
スコアの差が大きいと高い確率で良い席を選び、小さいとランダム性が強くなります。
結果:座席の選ばれ方をヒートマップで可視化
100人の乗客が1人ずつ座席を選んだ結果、どの席が何回選ばれたかを集計し、ヒートマップで表示します。
heatmap(
counts',
c=:blues,
xlabel="座席番号 (1–10)",
title="座席選択頻度ヒートマップ(100人)",
colorbar_title="回数"
)

見えてきた傾向
- 端の席(Seat 1 や Seat 10)は人気が高め
- 女子高生(Seat 3)や大柄な男性(Seat 7)の「隣」は避けられる傾向
- 観察学習によって、誰も座らない席はより避けられるようになる
拡張性
このモデルは非常にシンプルですが、以下のような拡張が考えられます:
- 2列×5の対面座席で空間配置を複雑にする
- 通勤ラッシュやガラガラの時間帯など状況設定を変える
- 女子高生や大柄な男性の位置をランダムにする
- 観察学習の強さを調整して社会的影響力を分析
おわりに
「座るだけ」の行動にも、無意識の心理や社会的な気遣いが詰まっています。
こうした身近な行動をモデル化してシミュレーションすることで、行動の背景にある「見えないルール」を読み解くことができます。
技術メモ
- 使用言語:Julia
- 使用ライブラリ:Plots.jl, Distributions.jl
- モデリング手法:エージェントベースモデル + softmax選択 + 観察学習
ちょっとした遊び心から始めたプロジェクトでしたが、意外と深い考察にたどり着けました。次は、座席予約システムや対面座席編もやってみたいですね。
コード全文
using Random using Distributions # ← Categorical分布用 using Plots default(fontfamily="MS Gothic") ######################### # softmax を自作 ######################### function softmax(x::Vector{Float64}; beta::Float64=1.0) exps = exp.((x .- maximum(x)) .* beta) # 安定化のため最大値引く return exps ./ sum(exps) end ######################### # 型定義 ######################### struct Passenger name::String sociability::Float64 sensitivity::Float64 observational::Float64 end mutable struct Seated label::String category::Symbol end ######################### # スコア計算 ######################### function seat_score(seats, skipped, idx, person::Passenger) seats[idx] !== nothing && return -Inf s = 0.0 n = length(seats) if idx == 1 || idx == n s += 0.5 end if (idx == 1 || seats[idx - 1] === nothing) && (idx == n || seats[idx + 1] === nothing) s += person.sociability end for j in (-1, 1) k = idx + j if 1 <= k <= n && seats[k] isa Seated nb = seats[k] if nb.category == :jk s -= 1.5 * person.sensitivity elseif nb.category == :big s -= 1.0 * person.sensitivity end end end s -= 0.05 * person.observational * skipped[idx] return s end ######################### # 着席処理(ソフトマックスで確率選択) ######################### function seat_one!(seats, skipped, counts, person::Passenger; beta=2.0) scores = [seat_score(seats, skipped, i, person) for i in 1:length(seats)] probs = softmax(scores; beta=beta) sel = rand(Categorical(probs)) seats[sel] = Seated(person.name, :normal) counts[sel] += 1 for i in 1:length(seats) seats[i] === nothing && (skipped[i] += 1) end seats[sel] = nothing # 降車(JK/BIG以外) end ######################### # 初期化 ######################### n_seats = 10 n_passengers = 100 seats = Vector{Union{Nothing, Seated}}(fill(nothing, n_seats)) skipped = zeros(Int, n_seats) counts = zeros(Int, n_seats) seats[3] = Seated("JK", :jk) seats[7] = Seated("BIG", :big) passengers = [Passenger("P$i", rand(), rand(), rand()) for i in 1:n_passengers] ######################### # シミュレーション ######################### for p in passengers seat_one!(seats, skipped, counts, p) end println("各席の選ばれた回数:", counts) ######################### # ヒートマップ ######################### heatmap( counts', c=:blues, xlabel="座席番号 (1–10)", title="座席選択頻度ヒートマップ(100人)", colorbar_title="回数" )