Quick introduction: Evolutionary game assay in Python

It’s been a while since I’ve shared or discussed code on TheEGG. So to avoid always being too vague and theoretical, I want to use this post to explain how one would write some Python code to measure evolutionary games. This will be an annotated sketch of the game assay from our recent work on measuring evolutionary games in non-small cell lung cancer (Kaznatcheev et al., 2019).

The motivation for this post came about a month ago when Nathan Farrokhian was asking for some advice on how to repeat our game assay with a new experimental system. He has since done so (I think) by measuring the game between Gefitinib-sensitive and Gefitinib-resistant cell types. And I thought it would make a nice post in the quick introductions series.

Of course, the details of the system don’t matter. As long as you have an array of growth rates (call them yR and yG with corresponding errors yR_e and yG_e) and initial proportions of cell types (call them xR and xG) then you could repeat the assay. To see how to get to this array from more primitive measurements, see my old post on population dynamics from time-lapse microscopy. It also has Python code for your enjoyment.

In this post, I’ll go through the two final steps of the game assay. First, I’ll show how to fit and visualize fitness functions (Figure 3 in Kaznatcheev et al., 2019). Second, I’ll transform those fitness functions into game points and plot (Figure 4b in Kaznatcheev et al., 2019). I’ll save discussions of the non-linear game assay (see Appendix F in Kaznatcheev et al., 2019) for a future post.

As always, it is good to start with some imports and visualize our data:

import numpy as np
from matplotlib import pyplot as plt
from scipy.stats import mstats

r_GC = 'c' #colour for R-type cells
g_GC = 'm' #colour for G-type cells

plt.errorbar(xR,yR,yerr=yR_e,fmt=r_GC+'s',markersize=ms,fillstyle='none')
plt.errorbar(xG,yG,yerr=yG_e,fmt=g_GC+'s',markersize=ms,fillstyle='none')

At this point, you’ll have a plot of growth-rates versus initial proportion. This is what you need to get fitness functions. And at this point your first choice enters into the analysis. Based on your knowledge of the system and the goal of your measurement, you should decide on what sort of class of functions should be considered as candidates for the fitness functions. I’ll go into details of this in the future post, but if our goal is to measure matrix (i.e. linear) games then we should choose linear functions.

So, let’s fit some linear functions and visualize.

[ra,rb], [[rav,rabv],[_,rbv]] = np.polyfit(xR,yR,1,w=1/yR_e**2,full=False,cov=True)
[ga,gb], [[gav,gabv],[_,gbv]] = np.polyfit(xG,yG,1,w=1/yG_e**2,full=False,cov=True)

plt.plot([0,1],[rb,rb + ra],r_GC+'--')
plt.plot([0,1],[gb,gb + ga],g_GC+'--')

If you were to do this with our data for Alectinib-sensitive versus Alectinib-resistant non-small-cell lung cancer. And repeated it four times for four conditions (DMSO, DMSO + Fibroblasts, Alectinib, Alectinib + Fibroblasts) — plotting each condition in a new subfigure then (with a few extra axis labels, etc) you’d arrive at a graphic like the following:

Fitness functions

Now, the last step is to visualize these fitness functions as points in game-space. For this, we want to propagate our errors, so it’s helpful to define the following auxiliary functions:

def FitRightPoint(fit):
out = fit[0] + fit[1]
out_v = fit[2] + fit[3] + 2*fit[4]

return [out, out_v]

def IndepDiff(point1,point2):
out = point1[0] - point2[0]
out_v = point1[1] + point2[1]

return [out, out_v]

def GameCoord(fit_L,fit_R):
point_y = IndepDiff(fit_R[[1,3]],fit_L[[1,3]])
point_x = IndepDiff(FitRightPoint(fit_L),FitRightPoint(fit_R))

return point_x, point_y

Now, you can just calculate the game-point and plot:

[game_x,gxv],[game_y,gyv] = ...
GameCoord(np.array([ra, rb, rav, rbv, rabv]), np.array([ga, gb, gav, gbv,gabv]))
game_xe = np.sqrt(gxv) #convert variance to error
game_ye = np.sqrt(gyv) #convert variance to error

plt.figure() #make a new figure, to avoid overwriting our fitness function plot

plt.errorbar(game_x,game_y,xerr = game_xe, yerr = game_ye, fmt='k.')

Game SpaceAgain, if you repeat this four times and add some cosmetic colours for the points and labels then you’ll end up with a nice game space plot. At right, you can see what that looks like for our non-small-cell lung cancer system.

I think that any measurement of a game is interesting — since we have so few in cancer. However, the measurements are even more exciting if game points for different conditions end up in different quadrants on the game space. Each quadrant corresponds to a qualitatively different kind of dynamic, so seeing games in different quadrants means that we’re seeing a switch in the type of interaction. This can be especially important if we’re aiming to treat the game instead of the player.

The most interesting quadrants are probably the top right and bottom left, where internal fixed points exist. The fixed point is fixed in the top right and unstable in the bottom left. I’ve yet to measure a system with a game in the bottom-left corner. I’ll be sure to report back when we find out.

If you measure some evolutionary games with the assay, dear reader, then please let me know!

And I hope this code sketch is useful. I’ll soon post more detailed game assay code on GitHub.

References

Kaznatcheev, A., Peacock, J., Basanta, D., Marusyk, A., & Scott, J. G. (2019). Fibroblasts and Alectinib switch the evolutionary games played by non-small cell lung cancer. Nature Ecology & Evolution, bioRxiv: 179259.

About Artem Kaznatcheev
From the Department of Computer Science at Oxford University and Department of Translational Hematology & Oncology Research at Cleveland Clinic, I marvel at the world through algorithmic lenses. My mind is drawn to evolutionary dynamics, theoretical computer science, mathematical oncology, computational learning theory, and philosophy of science. Previously I was at the Department of Integrated Mathematical Oncology at Moffitt Cancer Center, and the School of Computer Science and Department of Psychology at McGill University. In a past life, I worried about quantum queries at the Institute for Quantum Computing and Department of Combinatorics & Optimization at University of Waterloo and as a visitor to the Centre for Quantum Technologies at National University of Singapore. Meander with me on Google+ and Twitter.

4 Responses to Quick introduction: Evolutionary game assay in Python

  1. Pingback: The utter of Python to measure evolutionary games between cancer cells from in vitro experiments. – Python | Automate Python

  2. Pingback: Description before prediction: evolutionary games in oncology | Theory, Evolution, and Games Group

  3. Thanks for writing this Artem, it’s a very useful resource. It’s all really exciting work!

    Quick question– In GameCoord, I think it should be: [ga, gb, gav, gbv, gabv] (right now “gav” argument is repeated twice in the second line of the last code block). See here:

    GameCoord(np.array([ra, rb, rav, rbv, rabv]), np.array([ga, gb, gav, gav,gabv]))

    • Thanks Jeffrey, you are correct. I’ll edit the post to fix it!

      Thankfully that was a typo that was contained to just the blog post and doesn’t appear in the actual code I used for the paper (where these values were passed around more carefully with arrays).

      Good eye.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.