Maximizing the 10-Year Total with Python
The Problem
A country has several cities with very different characteristics. Industrial cities offer plenty of jobs but suffer from a housing shortage. Agricultural cities produce abundant food but offer few jobs. We can relocate residents between cities, and we must decide how many people to move where so that the total population after 10 years is as large as possible.
A Concrete Example
We model three cities (unit: thousand people).
| City | Initial population | Housing (year 0) | Housing added per year | Jobs (year 0) | Jobs added per year | Food production (people fed) |
|---|---|---|---|---|---|---|
| Industrial | 500 | 520 | 30 | 900 | 30 | 400 |
| Agricultural | 300 | 500 | 4 | 200 | 3 | 900 |
| Commercial | 200 | 330 | 8 | 380 | 10 | 300 |
Food is a national resource shared by everyone and grows by 1% per year.
The Mathematical Model
For city $i$ in year $t$, with population $P_{i,t}$, housing capacity $H_{i,t}$, and job capacity $J_{i,t}$, we define three satisfaction factors:
$$
h_{i,t}=\min!\left(1,\frac{H_{i,t}}{P_{i,t}}\right),\qquad
j_{i,t}=\min!\left(1,\frac{J_{i,t}}{P_{i,t}}\right),\qquad
f_{t}=\min!\left(1,\frac{\sum_i F_{i,t}}{\sum_i P_{i,t}}\right)
$$
The overall quality of life is
$$
q_{i,t}=h_{i,t},\bigl(0.6+0.4,j_{i,t}\bigr),f_t
$$
and the yearly population update is
$$
P_{i,t+1}=P_{i,t}\Bigl(1+b,q_{i,t}-d\Bigr),\qquad b=0.04,; d=0.01
$$
Let $m_{ij}$ be the number of people moving from city $i$ to city $j$ at the start. A share $\lambda=0.04$ of the arriving migrants is lost while settling, so the starting population becomes
$$
P_{j,0}’=P_{j,0}-\sum_{k\neq j} m_{jk}+(1-\lambda)\sum_{i\neq j} m_{ij}
$$
The optimization problem is
$$
\max_{m_{ij}\ge 0}\ \sum_{i=1}^{3}P_{i,10}\qquad \text{subject to}\qquad \sum_{j\neq i}m_{ij}\le 0.6,P_{i,0}
$$
To make the search unconstrained, each city gets a leaving ratio and a split ratio between its two destinations, giving six decision variables. Because the objective is non-convex and full of $\min$ kinks, we use differential evolution, a global optimizer.
Source Code
The simulation is fully vectorized with NumPy, so the 3,721 scenarios for the 3D surface are evaluated in a single call and the whole script finishes in a few seconds.
1 | import numpy as np |
Code Walkthrough
1. Problem settings
All city parameters are stored as NumPy arrays of length three, so every formula in the model is applied to all cities at once. P0 is the initial population, H0/dH describe housing and its yearly growth, J0/dJ do the same for jobs, and F0 is food production. MOVE_LOSS is the settling loss $\lambda$.
2. The simulation function
simulate implements the update rule $P_{i,t+1}=P_{i,t}(1+bq_{i,t}-d)$. Three details matter:
- Vectorization.
pop_startmay be a single vector of shape(3,)or a matrix of shape(N, 3)holding $N$ scenarios. The food factor usessum(axis=-1, keepdims=True), which works for both shapes. This is the key to the speed-up: instead of looping over thousands of scenarios in Python, NumPy evaluates them all in one pass. - Min functions.
np.minimum(1.0, H / P)caps each satisfaction factor at one. Extra housing above the population does not make life better, but a shortage reduces growth. - Time-varying capacity. Housing and jobs grow linearly, and food grows geometrically, so a good migration plan must anticipate the future, not just today’s situation.
3. The migration model
The six decision variables are encoded as [out_I, out_A, out_C, split_I, split_A, split_C]. flow_matrix converts them into a $3\times3$ matrix $M$, where $M_{ij}$ is the number of people moving from city $i$ to $j$. Because a city can never send away more than 60% of its residents, every candidate is automatically feasible, and no constraint handling is needed. after_migration subtracts the leavers, adds the arrivals after the settling loss, and returns the new starting population.
4. Optimization
differential_evolution is a population-based global optimizer. It is well suited here because the objective has kinks from the $\min$ operations and may have several local optima. A fixed seed makes the result reproducible, and polish=True refines the final answer with a local search.
5. Comparison strategies
Two baselines put the optimum in context: no migration, and a rule of thumb that simply moves 20% of the agricultural population to the industrial city, following the intuition that jobs are where the industry is.
6. The 3D surface
To visualize the landscape, we vary two flows, Agricultural → Industrial and Agricultural → Commercial, over a $61\times61$ grid while keeping the other flows at their optimal values. All 3,721 scenarios are stacked into one (3721, 3) array and simulated in a single call. Combinations that would move more people than the agricultural city has are masked with NaN.
7. One combined figure
Six panels are drawn on a single canvas, so one plt.show() produces the entire visual summary.
Console Output
================================================================
Population Migration and Urban Growth (unit: thousand people)
================================================================
City Initial No move Rule Optimal
Industrial 500.0 672.0 745.8 684.5
Agricultural 300.0 381.1 312.1 241.3
Commercial 200.0 268.8 268.8 411.4
----------------------------------------------------------------
Total 1000.0 1321.8 1326.7 1337.3
Optimal migration flows (rows: from, columns: to, thousand people)
Industrial Agricultural Commercial
Industrial 0.0 0.0 0.0
Agricultural 9.7 0.0 110.6
Commercial 0.0 0.0 0.0
Gain over no migration : 15.4 thousand (1.17 %)
Gain over rule of thumb: 10.6 thousand (0.80 %)
Grid best (A->I, A->C): (20.0, 100.0) -> total 1337.3 thousand
Graph Output

Interpreting the Results
Overall comparison
Doing nothing yields about 1,322 thousand people after 10 years. The simple rule of thumb of sending 20% of the farmers to the industrial city reaches about 1,327 thousand, while the optimized plan reaches about 1,337 thousand. The optimum beats the rule of thumb by roughly 0.8%. In a model where the baseline already grows by about 32%, that is a meaningful extra gain obtained purely by relocating people at the start.
The optimal plan
The optimizer moves almost nobody into the industrial city. Instead, it sends roughly 111 thousand people from the agricultural city to the commercial city and only about 10 thousand to the industrial city. This is the opposite of the naive intuition. The industrial city has many jobs, but its housing is already tight, so extra residents there are penalized by the housing factor $h_{i,t}$. The commercial city strikes the best balance between jobs and housing, so it absorbs most of the newcomers.
The 3D surface and contour map
The surface has a clear peak, and the red marker sits on top of it. Moving too few people leaves the agricultural city’s spare housing unused, while moving too many pushes the destination into overcrowding, where growth collapses. The ridge runs roughly along the line where the total number of migrants stays constant. This shows that the total number of migrants matters more than how they are split, with the split fine-tuning the result. The contour map shows the same shape from above and makes the optimal region easy to read.
Flow heatmap
The heatmap confirms that the optimal plan is essentially a single-corridor flow from the agricultural city to the commercial city, with all other flows near zero.
City populations
Compared with the dashed no-migration curves, the commercial city grows far faster under the optimal plan, while the agricultural city shrinks. The industrial city ends up close to its no-migration level, which is exactly the point: it was already near its comfortable capacity.
Total population by strategy
The optimal curve starts slightly lower because of the settling loss, then overtakes the other strategies as the better housing match compounds year after year. This is a classic trade-off: a small up-front cost for a larger long-term gain.
Housing pressure
The ratio of population to housing capacity shows why the plan works. Under no migration, the industrial city is already above 1, meaning a housing shortage, and keeps growing worse. Under the optimal plan, the pressure is spread more evenly across cities, which keeps the quality factor $q_{i,t}$ high and growth strong everywhere.
Conclusion
Maximizing long-term population is not about sending people to where the jobs are, nor to where the food is. It is about matching people to places that can both employ and house them, while accounting for how capacity changes over time. By combining a simple growth model, a vectorized simulation, and differential evolution, we found a migration plan that beats both doing nothing and a plausible rule of thumb. The same framework extends easily to more cities, yearly migration decisions, or additional constraints such as transport capacity and budgets.










