Skip to content

15 min read

The NK model, step by step, with nkscape

By

  • NK model
  • Agent-based modeling
  • Python
  • Open source
The NK model, step by step, with nkscape

Last week I published nkscape, a Python library for Kauffman NK fitness landscapes and NK-based agent-based models of organizations. It is open source under the MIT licence and on PyPI:

pip install nkscape 

The library is the short part of this post. The long part is what an NK model is, the way I learned it before writing a line of code: with a firm that has three decisions, tables you can check by hand and one cube.

nkscape in short

A model consists of a landscape, agents and population rules. The landscape holds N binary decisions, an interaction matrix that says which decisions affect which payoffs, and random payoff tables. An agent is the search rule of one organization; you subclass it and write one method. A population rule, such as selection or a central choice, is applied to the whole population once per period. nk.run runs the periods and records every configuration, its fitness and who was alive.

There is no global random state: every function takes a seed, and an agent walks the same way alone as in a crowd. For N up to 24 the library holds the fitness of all 2^N configurations at once, so peaks and the global maximum are exact. It is tested, typed and documented, and a notebook reproduces Figures 1 to 9 of Levinthal (1997).

import nkscape as nk

landscape = nk.Landscape.next_k(10, 3, rng=42)  # N = 10 decisions, K = 3
print(landscape.count_peaks())  # 14

agents = [nk.agents.HillClimber("first") for _ in range(100)]
result = nk.run(landscape, agents, periods=20, rng=1)
print(result.fitness[:, -1].mean() / landscape.global_max())  # 0.944

A firm with three decisions

Picture a small firm facing three decisions, each answered with 0 (no) or 1 (yes):

  1. Sell online?
  2. Offer a premium product?
  3. Offer fast delivery?

A configuration is one answer to all three. 101 means online yes, premium no, fast delivery yes. With three decisions there are 2^3 = 8 configurations, and the number of decisions is what N stands for.

Case 1: the decisions don't affect each other (K = 0)

Each decision has its own small payoff table, one score for "no" and one for "yes":

Decision if 0 (no) if 1 (yes)
1, sell online? 0.3 0.7
2, premium product? 0.6 0.2
3, fast delivery? 0.4 0.9

Each score is a number between 0 and 1 that says how well that choice works out. I made these up; in the real model they are random numbers, because the model is not about any particular firm. It only needs some choices to be better than others.

The firm's performance, its fitness, is the average of the three scores. For 101: online yes gives 0.7, premium no gives 0.6, delivery yes gives 0.9, and (0.7 + 0.6 + 0.9) / 3 = 0.73. All eight configurations:

Configuration Scores picked Fitness
000 0.3, 0.6, 0.4 0.43
001 0.3, 0.6, 0.9 0.60
010 0.3, 0.2, 0.4 0.30
011 0.3, 0.2, 0.9 0.47
100 0.7, 0.6, 0.4 0.57
101 0.7, 0.6, 0.9 0.73
110 0.7, 0.2, 0.4 0.43
111 0.7, 0.2, 0.9 0.60

This case is easy. The score of "online" depends only on "online", so the firm can settle each decision on its own: yes to online, no to premium, yes to delivery. That gives 101, and nothing beats it. Wherever the firm starts, improving one decision at a time leads there. The landscape is one smooth hill. That is what K = 0 means: each score depends on zero other decisions.

Case 2: each decision affects another (K = 1)

Now each score also depends on one other decision. Selling online pays off differently depending on whether there is a premium product, premium pays off differently depending on delivery, and fast delivery pays off differently depending on whether the firm is online. Each table needs four numbers instead of two, one for every combination of its own answer and the other decision's answer.

Decision 1, sell online? Its score also depends on premium.

online premium score the story behind it
no no 0.8 simple local shop, works fine
no yes 0.1 premium product that nobody finds
yes no 0.2 cheap goods online means a price war
yes yes 0.9 premium brand online, great

Decision 2, premium product? Its score also depends on delivery.

premium delivery score the story behind it
no no 0.7 cheap and slow, low costs
no yes 0.2 express shipping eats the margin
yes no 0.3 premium customers hate waiting
yes yes 0.8 premium plus express fit together

Decision 3, fast delivery? Its score also depends on online.

delivery online score the story behind it
no no 0.6 local shop, nobody expects shipping
no yes 0.1 online customers expect speed
yes no 0.2 express logistics with no webshop
yes yes 0.9 online plus express, great

In case 1, "online = yes" always scored 0.7. Here it has no fixed score: it scores 0.2 or 0.9 depending on what the firm does about premium. That is what K = 1 means.

Take 101 again. For each decision, find the row that matches both answers:

Decision own answer the other decision it looks at score
1, online yes premium = no 0.2
2, premium no delivery = yes 0.2
3, delivery yes online = yes 0.9

Fitness is (0.2 + 0.2 + 0.9) / 3 = 0.43. All eight configurations:

Configuration Scores picked Fitness
000 0.8, 0.7, 0.6 0.70
001 0.8, 0.2, 0.2 0.40
010 0.1, 0.3, 0.6 0.33
011 0.1, 0.8, 0.2 0.37
100 0.2, 0.7, 0.1 0.33
101 0.2, 0.2, 0.9 0.43
110 0.9, 0.3, 0.1 0.43
111 0.9, 0.8, 0.9 0.87

Two combinations fit together: all no, a simple local shop at 0.70, and all yes, a premium online express brand at 0.87. With three decisions the whole landscape can be drawn as a cube. Each corner is a configuration and each edge changes exactly one decision:

The eight configurations as two cubes: one peak with K = 0, two peaks with K = 1

A peak, or local optimum, is a corner where every connected corner is lower. With K = 0 there is one, 101. With K = 1 there are two: 000 at 0.70 and 111 at 0.87.

Why the firm gets stuck

Suppose the firm is the local shop, 000, and tries only going online, which gives 100:

at 000 at 100 what happened
score of online 0.8 0.2 online without premium is a price war
score of premium 0.7 0.7 unchanged, it does not look at online
score of delivery 0.6 0.1 slow delivery is a problem now that the firm is online
fitness 0.70 0.33

One decision changed and two scores moved, because delivery's score looks at online. In case 1 a flip could only change its own score. The move looks like a mistake, so the firm reverses it. Trying only premium or only fast delivery goes the same way. The firm concludes it is already at its best and stays at 0.70. The 0.87 exists, but it is only reachable by changing everything at once.

That is a rugged landscape with a local optimum, and it is the core phenomenon that Levinthal, Rivkin and Siggelkow and Wall study: interdependent decisions make step-by-step improvement get trapped on a mediocre peak.

How many numbers a landscape needs

A landscape is nothing more than its random payoff numbers; the fitness values, the peaks and the getting stuck are all computed from them by lookup and averaging. Each decision's table needs one number for every combination of the decisions it looks at: itself plus K others, so 2^(K+1) rows.

K a score looks at rows per table
0 itself 2
1 itself and 1 other 4
2 itself and 2 others 8
3 itself and 3 others 16

In total N × 2^(K+1) numbers:

numbers to draw
our firm, N = 3, K = 1 3 × 4 = 12
Levinthal (1997), N = 10, K = 0 10 × 2 = 20
N = 10, K = 5 10 × 64 = 640
Rivkin and Siggelkow, N = 12, K = 11 12 × 4,096 = 49,152

At K = N - 1 every score looks at every decision; flipping any one decision changes every score and the landscape is as rugged as it can be. At K = 0 there is one smooth hill. K is the dial between the two.

Who looks at whom: the interaction matrix

In words, online looks at premium, premium at delivery and delivery at online. The papers write the same information as a grid. A row says what a score depends on, a column says which scores change when that decision flips:

           d1 d2 d3
  score 1   X  X  .
  score 2   .  X  X
  score 3   X  .  X

Every row has an X on the diagonal, because a score always looks at its own decision, and K is the number of X's in a row minus that one. Column d1 has X's in rows 1 and 3, which is exactly why flipping "online" moved the online score and the delivery score in the table above.

The grid matters beyond K. Here are two ways to fill a 6 × 6 grid with K = 2, so every row has three X's:

next-K                              random-K (Kauffman's original)
           d1 d2 d3 d4 d5 d6                   d1 d2 d3 d4 d5 d6
  score 1   X  X  X  .  .  .          score 1   X  X  .  .  X  .
  score 2   .  X  X  X  .  .          score 2   .  X  X  X  .  .
  score 3   .  .  X  X  X  .          score 3   X  .  X  .  X  .
  score 4   .  .  .  X  X  X          score 4   X  .  X  X  .  .
  score 5   X  .  .  .  X  X          score 5   .  .  X  X  X  .
  score 6   X  X  .  .  .  X          score 6   .  .  X  X  .  X

The rows are equally full, the columns are not. In the next-K grid every decision affects exactly three scores. In the random grid, d3 affects five scores and d6 only its own: flipping d3 shakes almost the whole firm, flipping d6 disturbs nothing else. The organization papers go further and give departments their own blocks of the grid, which is where decomposability, coordination and incentives come in. That is why nkscape stores the matrix as a real object and builds every landscape from one.

One seed, several random ingredients

A random number generator is a long list of numbers that is already fixed, with a bookmark in it. Every draw reads from the bookmark and moves it forward. The trap is one shared generator for everything. Build three landscapes and run a search on each, all drawing from one generator, and then change how many numbers the search uses:

search uses 2 random numbers per run:        search uses 5 random numbers per run:
  landscape 0: [0.77 0.44 0.86]                landscape 0: [0.77 0.44 0.86]
  landscape 1: [0.98 0.76 0.79]                landscape 1: [0.13 0.45 0.37]
  landscape 2: [0.37 0.93 0.64]                landscape 2: [0.55 0.06 0.83]

Landscapes 1 and 2 are different, although the seed and the landscape code did not change. The search moved the bookmark, so the next landscape started reading somewhere else. Compare two search strategies over a thousand landscapes this way and part of the difference you measure is one strategy getting easier landscapes.

The fix is separate streams. parent.spawn(2) gives two children that are statistically independent, and the same seed always gives the same children. In nkscape every random ingredient gets its own stream: the matrix, the tables, every agent, every rule. That is why an agent walks the same way alone as in a crowd.

How rugged: the number of peaks

For a small landscape the library can hold the fitness of every configuration at once, as an array with one axis of length 2 per decision, built by broadcasting the payoff tables. For the firm:

fitness cube (position = configuration):
[[[0.7  0.4 ]
  [0.33 0.37]]

 [[0.33 0.43]
  [0.43 0.87]]]
peaks: [[0, 0, 0], [1, 1, 1]] | count: 2
global maximum: 0.87

With that, counting peaks is cheap, and the first thing to look at is how the count grows with K. Here are 200 random landscapes per K with N = 10:

K peaks, mean fewest most
0 1.0 1 1
1 3.4 1 15
2 7.7 1 25
3 13.8 4 25
4 21.8 10 34
5 32.6 23 45
6 44.0 32 58
7 58.3 45 72
8 74.3 59 88
9 92.7 74 110

The number of peaks as K goes up, N = 10, 200 random landscapes per K

At K = N - 1 theory says 2^N / (N + 1) peaks on average, 93.1 for N = 10, and the measured 92.7 sits right there. The 2,000 landscapes took 0.6 seconds. The cube costs 8 bytes per configuration, so memory is what limits it: N = 24 needs 0.13 GB, N = 30 needs 8.6 GB, which is why the library refuses above 24 unless you raise the limit.

Walking the landscape

A period is one tick of the clock. In each period an agent looks around, decides, and moves or stays. Here are two hand-written rules, both starting at 010 on the firm's landscape, whose peaks are 000 at 0.70 and 111 at 0.87.

Rule A, best neighbour: each period, look at all three neighbours and move to the best one if it beats staying.

at 010 (0.33)  sees 110=0.43, 000=0.70, 011=0.37  ->  move to 000
at 000 (0.70)  sees 100=0.33, 010=0.33, 001=0.40  ->  nothing better -> stay
at 000 (0.70)  sees 100=0.33, 010=0.33, 001=0.40  ->  nothing better -> stay

After one move the agent stays at 000 forever. It took the largest immediate gain and that put it on the lower peak.

Rule B, one random try: each period, pick one decision at random and flip it if the result is better.

seed 1                                        seed 3
at 010 (0.33) tries d1: 000=0.70 -> move      at 010 (0.33) tries d2: 011=0.37 -> move
at 000 (0.70) tries d1: 010=0.33 -> stay      at 011 (0.37) tries d0: 111=0.87 -> move
at 000 (0.70) tries d2: 001=0.40 -> stay      at 111 (0.87) tries d0: 011=0.37 -> stay

Both runs start at the same place with the same rule and end on different peaks. With seed 1 the agent happened to try d1 first and landed on the lower peak. With seed 3 it tried d2 first, took a small gain from 0.33 to 0.37, and from there the path to the global peak was open. Two things follow: agents need randomness, and it has to be seeded, because the seed decides which peak the agent reaches. And greedy is not always better; rule A's bigger first step led to the worse peak.

One hand-made landscape proves little, so here are both rules on 200 landscapes with N = 10 and K = 3, 50 random starts each, 10,000 walks per rule:

Rule final fitness / global max reached the global max periods until stuck
A, best neighbour 0.935 23.7 % 3.1
B, one random try 0.926 20.3 % 21.5

Both rules get stuck below the top most of the time; only about one walk in five reaches the global maximum. That is the core finding of the rugged-landscape literature. Fitness is divided by the global maximum of each landscape, because raw fitness is not comparable across landscapes. And what an agent may do within one period, look at all neighbours or at one, is a modelling decision that every paper makes on its own, which is why in nkscape the agent defines its own period.

From the toy firm to Levinthal (1997)

Levinthal's paper does exactly this with N = 10, a hundred organizations per landscape and a hundred landscapes, and adds long jumps, selection and a landscape that changes halfway. The notebook in the repository reproduces its Figures 1 to 9; the red dots are the values read off the figures in the article.

Figure 1 of Levinthal (1997) reproduced with nkscape: distinct organizational forms over time for K = 0, 1 and 5

Some details are not in the article. The first version of my hill climber chose a better neighbour at random and was off by up to 19 forms in the early periods. Levinthal's original Turbo Pascal program is still online, and it settled it: neighbours are tried in a fixed order and the first better one is taken, which reproduces the figure. The program also answered how survival is decided, how newcomers are created and which noise formula Figure 9 needs.

Figure 8 of Levinthal (1997) reproduced with nkscape: the number of local peaks by K

What is in the library

Everything above: nk.Landscape holds the matrix and the tables, nk.Agent is a rule like A or B, nk.run runs the periods and nk.agents.HillClimber is the ready-made climber with Levinthal's long jumps. What is not in it yet, and what I want to add as time allows: departments that own subsets of the decisions, with the coordination modes and incentive schemes of Rivkin and Siggelkow (2003) and Wall (2016), satisficing agents, an experiment runner and plots.

More posts