フロー捕捉型施設配置問題(FCLM)入門¶

1. はじめに¶

ガソリンスタンドやコンビニエンスストア、フードトラックのようなサービスでは、顧客は「わざわざ施設に行く」のではなく「移動ルートの途中で立ち寄る」ことが多い。このような施設の立地では、人口の多い地域をカバーしているかどうかよりも、交通流の太い経路上に立地しているかどうかが収益に直結する。

このように「流れ(フロー)をどれだけ自施設に引き込めるか」を最大化する施設配置問題をフロー捕捉型施設配置問題(Flow-Capturing Location-Allocation Model、FCLM)と呼ぶ。p-median 問題やLSCP が「地域に固定された需要」を対象とするのに対し、FCLM は「ネットワーク上を移動する需要」を対象とする点が特徴である。移動体通信の基地局配置など、経路上でサービスを受ける状況に広く応用できる。

2. 前提知識と学習目標¶

本資料を読む前に知っておくとよいことは次のとおりである。

  • 数学(高校1〜2年程度): 一次方程式、集合、総和記号。
  • プログラミング: if 文による条件分岐、for 文による繰り返し、list によるデータの管理。
  • グラフ理論: ノードとエッジという用語(詳しい知識は不要)。

学習目標¶

  1. FCLM が「流量の捕捉」を最大化するモデルであることを具体例で説明できる。
  2. 目的関数と制約条件を自力で組み立てられる。
  3. PuLP で小規模ネットワークの問題を実装し、最適解を確認、可視化できる。

3. 問題設定と定式化¶

3.1 問題の具体例¶

5つのノード(1〜5)が順に道路で結ばれ、さらにノード2と4を直接結ぶ道路がある小さなネットワークを考える。このネットワーク上を5本の交通流(経路)が流れている。経路 $j$ は、通過するノードの列 $N_j$ と1日あたりの交通量 $f_j$ で表す。

経路 $j$ 通過ノード $N_j$ 交通量 $f_j$ [人/日]
1 1, 2 120
2 2, 3 150
3 3, 4 80
4 4, 5 100
5 2, 4 60

施設はネットワークのノード上に建てる。経路 $j$ の通過ノードのどこかに施設が1つでもあれば、その経路の交通量をすべて捕捉できると仮定する。複数の施設が同じ経路上にあっても、交通量は重複して数えない(1回のみ数える)。施設をちょうど $p = 2$ か所に設置するとき、どのノードに設置すれば捕捉できる交通量の合計が最大になるだろうか。

3.2 数学的な定式化¶

3.2.1 定数および決定変数の定義¶

名前 説明
$I$ ノードの集合(経路上に現れるノード全体、この例では1〜5)
$J$ 経路の集合(この例では5本)
$N_j$ 経路 $j$ が通過するノードの集合
$f_j$ 経路 $j$ の交通量
$p$ 設置する施設数(この例では2)
$x_i$ ノード $i$ に施設を設置するなら1、そうでなければ0
$y_j$ 経路 $j$ が捕捉されるなら1、そうでなければ0

3.2.2 数理モデル¶

$$ \begin{align} \max \ \ & z = \sum_{j \in J} f_j y_j \\ \text{s.t. } \ \ & \sum_{i \in N_j} x_i \geq y_j, \quad \forall j \in J, \\ & \sum_{i \in I} x_i = p, \\ & x_i, y_j \in \{0, 1\}, \quad \forall i \in I, \forall j \in J. \end{align} $$

3.2.3 目的関数の意味¶

目的関数は捕捉した経路の交通量の合計であり、これを最大化する。この値は「店舗の前を通過する潜在顧客数」の概数を意味し、売上予測などの代理指標として使われる。

3.2.4 制約条件の意味¶

1本目の制約条件は捕捉の判定を表す。経路 $j$ の通過ノードに施設が1つも設置されていなければ左辺は0となり、$y_j = 0$ が強制される。つまり「施設がないのに流れを捕捉したことにする」解を排除している。逆に通過ノード上に施設が1つでもあれば $y_j = 1$ を選べるので、最大化の圧力により最適解では必ず $y_j = 1$ となる。このモデルに必要なデータは各経路の通過ノード集合 $N_j$ と交通量 $f_j$ だけであり、ノードと経路の被覆関係を表す行列を別途作る必要はない。

2本目は設置する施設数の指定であり、設備投資の制約を表す。3本目は各変数が0か1のみを取ることを表し、これにより問題全体は0-1整数計画問題となる。

LSCP との対比で見ると、LSCP が「すべての需要をカバーする最小の施設数」を求めるのに対し、FCLM は「施設数の制約のもとで捕捉量を最大化」する。この「カバー量最大化」型のモデルの原型は最大被覆問題(MCLP)であり、FCLM は需要の定義を「地域上の点」から「ネットワーク上の流れ」に読み替えたMCLP の拡張といえる。

4. Python + PuLP による実装¶

4.1 準備¶

以下にFCLM の実装例を示す。問題は経路5本、ノード5個と小規模なので、施設の組合せを全通り書き出せば手でも解ける。ぜひ試してみてほしい。

なおGoogle Colab で実行する際には、必ず最初に以下のプログラム2行目のコメントを外して実行すること。

In [1]:
# Google Colab の場合は2行目のコメントを外して実行: ライブラリインストール(1回だけ)
# !pip install pulp matplotlib networkx
In [2]:
import pulp
import matplotlib.pyplot as plt
import networkx as nx

# 経路データ: N[j-1] が経路j の通過ノード列、f[j-1] が経路j の交通量 [人/日]
N = [[1, 2], [2, 3], [3, 4], [4, 5], [2, 4]]
f = [120, 150, 80, 100, 60]

I = sorted(set(sum(N, [])))     # 経路上に現れるノードの集合
J = list(range(1, len(N) + 1))  # 経路の番号
p = 2                           # 設置する施設数

print(f'ノード集合 I = {I}')
print(f'経路数: {len(J)}, 設置数 p = {p}')
ノード集合 I = [1, 2, 3, 4, 5]
経路数: 5, 設置数 p = 2

4.2 モデル構築¶

In [3]:
# 最大化問題として宣言
prob = pulp.LpProblem('FCLM_example', pulp.LpMaximize)

# 変数
x = pulp.LpVariable.dicts('x', I, cat='Binary')  # ノードへの施設設置
y = pulp.LpVariable.dicts('y', J, cat='Binary')  # 経路の捕捉

# 目的関数: 捕捉した交通量の合計
prob += pulp.lpSum(f[j - 1] * y[j] for j in J)

# 制約1: 捕捉の判定(経路j の通過ノードに施設がなければ y_j = 0)
for j in J:
    prob += pulp.lpSum(x[i] for i in N[j - 1]) >= y[j]

# 制約2: 施設数の指定
prob += pulp.lpSum(x[i] for i in I) == p

print(prob)
FCLM_example:
MAXIMIZE
120*y_1 + 150*y_2 + 80*y_3 + 100*y_4 + 60*y_5 + 0.0
SUBJECT TO
_C1: x_1 + x_2 - y_1 >= 0

_C2: x_2 + x_3 - y_2 >= 0

_C3: x_3 + x_4 - y_3 >= 0

_C4: x_4 + x_5 - y_4 >= 0

_C5: x_2 + x_4 - y_5 >= 0

_C6: x_1 + x_2 + x_3 + x_4 + x_5 = 2

VARIABLES
0 <= x_1 <= 1 Integer
0 <= x_2 <= 1 Integer
0 <= x_3 <= 1 Integer
0 <= x_4 <= 1 Integer
0 <= x_5 <= 1 Integer
0 <= y_1 <= 1 Integer
0 <= y_2 <= 1 Integer
0 <= y_3 <= 1 Integer
0 <= y_4 <= 1 Integer
0 <= y_5 <= 1 Integer

表示されているのはpulp によって生成された問題の中身である。3.2.2 の数理モデルと1行ずつ見比べてみてほしい。

4.3 求解および結果の解釈¶

In [4]:
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print('求解結果:', pulp.LpStatus[prob.status])
print('捕捉交通量の最大値:', int(pulp.value(prob.objective)), '[人/日]')
print('設置ノード:', [i for i in I if x[i].value() == 1])
print('捕捉された経路:', [j for j in J if y[j].value() == 1])
求解結果: Optimal
捕捉交通量の最大値: 510 [人/日]
設置ノード: [2, 4]
捕捉された経路: [1, 2, 3, 4, 5]

最適解はノード2と4に施設を設置する配置であり、このとき5本の経路すべてが捕捉され、捕捉交通量は510 [人/日] (全交通量)となる。ノード2は経路1、2、5の3本が、ノード4は経路3、4、5の3本が通過する合流点であり、この2か所でネットワーク上の全経路を覆える。カバーの「広さ」ではなく「どれだけ多くの経路が通るか」が効く、という流れ捕捉型モデルらしい結果である。

4.4 可視化¶

In [5]:
# ネットワークと解の可視化(この例では各経路が1本の辺に対応する)
G = nx.Graph()
for Nj in N:
    G.add_edge(Nj[0], Nj[-1])
pos = {1: (0, 0), 2: (1, 0), 3: (2, 0.6), 4: (3, 0), 5: (4, 0)}

captured = [j for j in J if y[j].value() == 1]
chosen = [i for i in I if x[i].value() == 1]

plt.figure(figsize=(8, 3))
for j in J:
    u, v = N[j - 1][0], N[j - 1][-1]
    color = 'red' if j in captured else 'lightgray'
    nx.draw_networkx_edges(G, pos, edgelist=[(u, v)], edge_color=color, width=2.5)
nx.draw_networkx_edge_labels(
    G, pos, edge_labels={(N[j - 1][0], N[j - 1][-1]): f[j - 1] for j in J}, font_size=9)
nx.draw_networkx_nodes(G, pos, node_size=320, node_color='white', edgecolors='black')
nx.draw_networkx_nodes(G, pos, nodelist=chosen, node_size=320, node_color='red',
                       edgecolors='black')
nx.draw_networkx_labels(G, pos, font_size=10)
plt.axis('off')
plt.tight_layout()
plt.show()
No description has been provided for this image

赤いノードが施設を設置したノード、赤い辺が捕捉された経路である。

4.5 感度分析: 施設数を変える¶

設置数 $p$ を変えると最適値はどう変わるだろうか。$p = 1, 2, 3$ について解き比べてみる。

In [6]:
def solve_fclm(N, f, p):
    I = sorted(set(sum(N, [])))
    J = list(range(1, len(N) + 1))
    prob = pulp.LpProblem('FCLM', pulp.LpMaximize)
    x = pulp.LpVariable.dicts('x', I, cat='Binary')
    y = pulp.LpVariable.dicts('y', J, cat='Binary')
    prob += pulp.lpSum(f[j - 1] * y[j] for j in J)
    for j in J:
        prob += pulp.lpSum(x[i] for i in N[j - 1]) >= y[j]
    prob += pulp.lpSum(x[i] for i in I) == p
    prob.solve(pulp.PULP_CBC_CMD(msg=0))
    return int(pulp.value(prob.objective)), [i for i in I if x[i].value() == 1]

for p_test in [1, 2, 3]:
    z, chosen = solve_fclm(N, f, p_test)
    print(f'p = {p_test}: 捕捉交通量 = {z} [人/日], 設置ノード = {chosen}')
p = 1: 捕捉交通量 = 330 [人/日], 設置ノード = [2]
p = 2: 捕捉交通量 = 510 [人/日], 設置ノード = [2, 4]
p = 3: 捕捉交通量 = 510 [人/日], 設置ノード = [1, 2, 4]

$p=1$ では捕捉交通量は330 [人/日] (ノード2のみ)にとどまるが、$p=2$ で510 [人/日]に増え、全経路を捕捉できる。一方 $p=2$ から $p=3$ に増やしても捕捉交通量は510 のまま変わらない。すでに全交通量を捕捉しているため、3つ目の施設はどこに置いても追加の価値がなく、$p=3$ の最適解は一意でない。このように「施設を1つ増やしたときの効果」を定量的に示せることが、FCLM を実務で使う際の大きな利点である。

5. 道路ネットワークへの適用¶

ここまでの例は5ノードの小さなネットワークだったが、同じモデルは道路ネットワーク上のフローにそのまま適用できる。ここでは「道路ネットワークデータの扱い方」(optimization/road-network/road_network.ipynb)で生成したダミー道路網と経路データを使う。経路データは同資料で N.csv(行 $j$ が経路 $j$ の通過ノード列)と f.csv(行 $j$ が経路 $j$ の交通量)に保存してあり、これを読み込めば、あとは前節とまったく同じである。ノード集合 $I$ も前節と同じく、経路上に現れるノード全体とする。

In [7]:
import numpy as np

# 道路網の読み込み(描画用。road_network.ipynb と同じ手順)
edgs = np.loadtxt('../road-network/links_ud.csv', delimiter=',')
G = nx.Graph()
G.add_weighted_edges_from([(int(e[0]), int(e[1]), e[2]) for e in edgs])
pos = {}
for e in edgs:
    pos[int(e[0])] = [e[3], e[4]]
    pos[int(e[1])] = [e[5], e[6]]

# 経路データの読み込み
N = []
with open('../road-network/N.csv', 'r') as fp:
    for row in fp.readlines():
        N.append([int(v) for v in row.strip().split(',') if len(v) > 0])
f = [int(v) for v in np.loadtxt('../road-network/f.csv')]

I = sorted(set(sum(N, [])))
J = list(range(1, len(N) + 1))
print(f'経路数: {len(J)}, ノード数 |I| = {len(I)}')
for j in J:
    print(f'経路{j}: f_{j} = {f[j - 1]}, N_{j} = {N[j - 1]}')
経路数: 7, ノード数 |I| = 26
経路1: f_1 = 120, N_1 = [1, 2, 3, 4, 5, 6]
経路2: f_2 = 100, N_2 = [25, 26, 27, 28, 29, 30]
経路3: f_3 = 90, N_3 = [1, 7, 8, 14, 20, 26, 25]
経路4: f_4 = 80, N_4 = [6, 12, 18, 24, 30]
経路5: f_5 = 150, N_5 = [3, 9, 15, 21, 27]
経路6: f_6 = 60, N_6 = [13, 14, 15, 16, 17, 18]
経路7: f_7 = 70, N_7 = [11, 12, 18, 24]

定式化も実装も前節と同じである。前節で作った solve_fclm をそのまま使い、設置数 $p = 2$ で解く。

In [8]:
z2, chosen2 = solve_fclm(N, f, 2)
print('p = 2 の最大捕捉交通量:', z2, '[人/日]')
print('設置ノード:', chosen2)
p = 2 の最大捕捉交通量: 480 [人/日]
設置ノード: [3, 18]

最適解はノード3と18に施設を設置する配置であり、捕捉交通量は480 [人/日]である。ノード3は経路1(ノード1から6へ)の途中にあり、同時に経路5(ノード3から27へ)の出発地でもあるため、2本の経路(合計270)をまとめて受け持つ。ノード18は東側を通る経路4、6、7の3本(合計210)の通り道である。念のため、施設の組合せを全列挙して最適値を確かめる。

In [9]:
from itertools import combinations

def captured_value(open_nodes):
    return sum(fj for Nj, fj in zip(N, f) if set(Nj) & set(open_nodes))

best = max(captured_value(cb) for cb in combinations(I, 2))
print('全列挙による p = 2 の最大捕捉交通量:', best)
print('数理モデルの解と一致:', best == z2)
全列挙による p = 2 の最大捕捉交通量: 480
数理モデルの解と一致: True
In [10]:
# 可視化: 赤い実線 = 捕捉された経路、青い破線 = 捕捉されない経路
captured = [j for j in J if set(N[j - 1]) & set(chosen2)]

plt.figure(figsize=(8, 6))
nx.draw_networkx_edges(G, pos, edge_color='lightgray')
for j in J:
    if j not in captured:
        nx.draw_networkx_edges(G, pos, edgelist=list(zip(N[j - 1][:-1], N[j - 1][1:])),
                               edge_color='steelblue', style='dashed', width=2)
for j in J:
    if j in captured:
        nx.draw_networkx_edges(G, pos, edgelist=list(zip(N[j - 1][:-1], N[j - 1][1:])),
                               edge_color='red', width=2)
nx.draw_networkx_nodes(G, pos, node_size=170, node_color='white', edgecolors='gray')
nx.draw_networkx_nodes(G, pos, nodelist=chosen2, node_size=300, node_color='red',
                       edgecolors='black')
nx.draw_networkx_labels(G, pos, font_size=7)
plt.axis('equal')
plt.axis('off')
plt.tight_layout()
plt.show()
No description has been provided for this image

赤いノードが施設を設置したノードである。経路が重なる区間は捕捉された経路の色を優先して描いている。前節と同じく、設置数 $p$ を変えて最適値の変化を見る。

In [11]:
for p_test in [1, 2, 3]:
    z, chosen = solve_fclm(N, f, p_test)
    print(f'p = {p_test}: 捕捉交通量 = {z} [人/日], 設置ノード = {chosen}')
p = 1: 捕捉交通量 = 270 [人/日], 設置ノード = [3]
p = 2: 捕捉交通量 = 480 [人/日], 設置ノード = [3, 18]
p = 3: 捕捉交通量 = 670 [人/日], 設置ノード = [1, 18, 27]

$p$ を増やすごとに捕捉交通量は270、480、670 [人/日]と増え、$p = 3$ で全7経路を捕捉できる。表示された解では、ノード1が経路1と3を、ノード18が経路4、6、7を、ノード27が経路2と5を受け持つ。全7経路を捕捉できる3ノードの組合せはこれだけではなく(例えばノード3、18、26でも670になる)、$p = 3$ の最適解は一意でない。どのノードが「太いフローの通り道」になっているかは、ネットワークの形と交通量の与え方で決まる。実際の道路網でも、幹線道路の合流点がこのような要衝になる。

6. 発展: 実データへの適用と研究紹介¶

この教材のダミー道路網は30ノードだが、実務では数千ノード規模の道路ネットワークに同じモデルを適用する。本研究室でも市販の道路ネットワークデータを用いて、実在の市域を対象にフロー捕捉型のモデルを解析している(データは著作権の制約から公開できないため、教材では同じ形式のダミーデータを使っている)。実務規模のネットワークではノード数が数千を超え、経路(フロー)の列挙自体が難しくなるため、経路生成の工夫やグラフの縮約などが研究課題となる。また、本研究室ではフローの需要をさらに時間軸つきで扱う休憩施設配置の研究(RALP、RALMTD)にも取り組んでいる。これはLSCP 側の拡張であり、詳しくは「集合被覆問題(LSCP)入門」の発展節を参照してほしい。

7. 参考文献¶

  • M. J. Hodgson, A flow-capturing location-allocation model, Geographical Analysis, vol.22, no.3, pp.270-279, 1990.

8. 注意事項¶

  • 捕捉した交通量は売上そのものではなく潜在顧客数である。売上の見積もりには購買転換率などとの掛け合わせが必要である。
  • 交通量が時間帯で変動する場合は、昼と夜で別のモデルを解くなどの工夫が必要である。
  • 定式化が理解できないときは、第3節の例(経路5本、ノード5個)で施設の組合せを全通り書き出し、それぞれの捕捉交通量を手計算してみること。プログラムの結果と一致すれば、定式化とプログラムの両方を確かめたことになる。それでも理解できない場合には指導教員まで相談すること。

9. 卒業研究概要への手引き¶

  • このテーマを卒業研究のテーマにしたい場合は、ここの対応をすること(必ず事前に相談すること)。
  • 以下、すべて含めて、A4用紙2枚にまとめること。
  • 厳密に守る必要はないが、文章量の比率を目安にすること(15%とは、A4用紙2枚分の15%という意味)。

1章 はじめに(文章量の比率: 15%)¶

  • 施設配置問題とは何か、FCLM以外にどのような施設配置問題があるのか(p-median問題、集合被覆問題など)まとめること。
  • フロー捕捉型施設配置問題に関する先行研究を紹介すること。
  • 施設配置問題を解く際に使われる整数計画法とは何か、まとめること。

2章 FCLM(文章量の比率: 30%)¶

  • FCLMの数理モデルについて、説明すること。需要を点ではなく交通の流れとして捉える考え方も説明すること。

3章 実験(文章量の比率: 40%)¶

3.1節 概要¶

  • 第5節の道路ネットワーク上のFCLMをベースに、経路データ(N.csvとf.csv)を自分で作り直した問題を解き、施設の配置を図示すること。
  • 施設数 $p$ を変えて解の変化を確認すること。

3.2節 結果と考察¶

  • 捕捉された交通量の合計がいくつになったか示すこと。
  • その捕捉交通量が何を意味しているか説明すること。
  • 図示した画像から、どのようなことが言えるか、考え説明すること。

4章 おわりに(文章量の比率: 15%)¶

  • 今回の実験に対する感想を記載すること。
  • 例えば、交通量の多そうな交差点に直感で施設を配置するのと数理モデルにより配置するのでは、どちらがよさそうか、また良い理由を記載すること。

参考文献¶

参考にした資料を、2〜3件記載すること。以下、書き方の例である。

  • [1] 柿本ほか, XXXに関する分析, XXX学会論文誌, 2020.
  • [2] XXXに関する情報, http://xxx.ddd.ttt.com, 2020年4月20日閲覧

本文中で引用する場合は「柿本らはXXXを実施している [1]。また、〜」のように、どこで引用したのか明白にすること。