# Quick introduction: Evolutionary game assay in Python

February 16, 2019 4 Comments

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:

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.')

Again, 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.

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

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

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.