尝试用 Python 写了个病毒传播模拟程序
病毒扩散仿真程序,用 python 也可以。
运行效果:
https://www.bilibili.com/video/av89012144/
概述
事情是这样的,B 站 UP 主 @ele 实验室,写了一个简单的疫情传播仿真程序,告诉大家在家待着的重要性,视频相信大家都看过了,并且 UP 主也放出了源码。
因为是 Java 开发的,所以开始我并没有多加关注。后来看到有人解析代码,发现我也能看懂,然后就琢磨用 Python 应该怎么实现。
Java 版程序浅析
一个人就是 1 个(x, y)坐标点,并且每个人有一个状态。
public class Person extends Point {
private int state = State.NORMAL;
}
在每一轮的迭代中,遍历每个人,每个人根据自身的状态,做出一定的动作,包括:
- 移动
- 状态变化
- 影响他人
这些动作的具体变更,取决于定义的各种系数。
一轮迭代完成,打印这些点,不同的状态对应不同的颜色。
绘图部分直接使用的 Java 绘图类 Graphics。
Python 版思路
如果我们想用 Python 实现应该怎么做呢?
如果完全复刻 Java 版本,则每次迭代需遍历所有人,并计算和他人距离,这就是 N^2 次计算。如果是 1000 个人,就需要循环 1 百万次。这个 Python 的性能肯定捉急。
不过 Python 有 numpy
,可以快速的操作数组。结合 matplotlib
则可以画出图形。
import numpy as np
import matplotlib.pyplot as plt
如何模拟人群
为了减少函数之间互相传参和使用全局变量,我们也来定义一个类:
class People(object):
def __init__(self, count=1000, first_infected_count=3):
self.count = count
self.first_infected_count = first_infected_count
self.init()
所有人的坐标数据就是 N 行 2 列的数组,同时伴随一定的状态:
def init(self):
self._people = np.random.normal(0, 100, (self.count, 2))
self.reset()
状态值和计时器也都是数组,同时每次随机选取指定数量的人感染:
def reset(self):
self._round = 0
self._status = np.array([0] * self.count)
self._timer = np.array([0] * self.count)
self.random_people_state(self.first_infected_count, 1)
这里关键的一点是,辅助数组的大小和人数保持一致,这样就能形成一一对应的关系。
状态发生变化的人才顺带记录时间:
def random_people_state(self, num, state=1):
"""随机挑选人设置状态
"""
assert self.count > num
# TODO:极端情况下会出现无限循环
n = 0
while n < num:
i = np.random.randint(0, self.count)
if self._status[i] == state:
continue
else:
self.set_state(i, state)
n += 1
def set_state(self, i, state):
self._status[i] = state
# 记录状态改变的时间
self._timer[i] = self._round
通过状态值,就可以过滤出人群,每个人群都是 people
的切片视图。这里 numpy
的功能相当强大,只需要非常简洁的语法即可实现:
@property
def healthy(self):
return self._people[self._status == 0]
@property
def infected(self):
return self._people[self._status == 1]
按照既定的思路,我们先来定义每轮迭代要做的动作:
def update(self):
"""每一次迭代更新"""
self.change_state()
self.affect()
self.move()
self._round += 1
self.report()
顺序和开始分析的略有差异,其实并不是十分重要,调换它们的顺序也是可以的。
如何改变状态
这一步就是更新状态数组 self._status
和 计时器数组 self._timer
:
def change_state(self):
dt = self._round - self._timer
# 必须先更新时钟再更新状态
d = np.random.randint(3, 5)
self._timer[(self._status == 1) & ((dt == d) | (dt > 14))] = self._round
self._status[(self._status == 1) & ((dt == d) | (dt > 14))] += 1
仍然是通过切片过滤出要更改的目标,然后全部更新。
这里具体的实现我写的非常简单,没有引入太多的变量:
在一定周期内的 感染者(infected),状态置为 确诊(confirmed)。 我这里简单假设了确诊者就被医院收治,所以失去了继续感染他人的机会(见下面)。如果要搞复杂点,可以引入病床,治愈,死亡等状态。
如何影响他人
影响别人是整个程序的性能瓶颈,因为需要计算每个人之间的距离。
这里继续做了简化,只处理感染者:
def infect_possible(self, x=0., safe_distance=3.0):
"""按概率感染接近的健康人
x 的取值参考正态分布概率表,x=0 时感染概率是 50%
"""
for inf in self.infected:
dm = (self._people - inf) ** 2
d = dm.sum(axis=1) ** 0.5
sorted_index = d.argsort()
for i in sorted_index:
if d[i] >= safe_distance:
break # 超出范围,不用管了
if self._status[i] > 0:
continue
if np.random.normal() > x:
continue
self._status[i] = 1
# 记录状态改变的时间
self._timer[i] = self._round
可以看到,距离的计算仍然是通过 numpy
的矩阵操作。但是需要对每一个感染者单独计算,所以如果感染者较多,python 的处理效率感人。
如何移动
_people
是一个坐标矩阵,只要生成移动距离矩阵 dt,然后它相加即可。我们可以设置一个可移动的范围 width
,把移动距离控制在一定范围内。
def move(self, width=1, x=.0):
movement = self.random_movement(width=width)
# 限定特定状态的人员移动
switch = self.random_switch(x=x)
movement[switch == 0] = 0
self._people = self._people + movement
这里还需要增加一个控制移动意向的选项,仍然是利用了正态分布概率。考虑到这种场景有可能会重用,所以特地把这个方法提取了出来,生成一个只包含 0 1 的数组充当开关。
def random_switch(self, x=0.):
"""随机生成开关,0 - 关,1 - 开
x 大致取值范围 -1.99 - 1.99;
对应正态分布的概率, 取值 0 的时候对应概率是 50%
:param x: 控制开关比例
:return:
"""
normal = np.random.normal(0, 1, self.count)
switch = np.where(normal < x, 1, 0)
return switch
输出结果
有了一切数据和变化之后,接下来最重要的事情自然就是图形化显示结果了。直接使用 matplotlib
的散点图就可以了:
def report(self):
plt.cla()
# plt.grid(False)
p1 = plt.scatter(self.healthy[:, 0], self.healthy[:, 1], s=1)
p2 = plt.scatter(self.infected[:, 0], self.infected[:, 1], s=1, c='pink')
p3 = plt.scatter(self.confirmed[:, 0], self.confirmed[:, 1], s=1, c='red')
plt.legend([p1, p2, p3], ['healthy', 'infected', 'confirmed'], loc='upper right', scatterpoints=1)
t = "Round: %s, Healthy: %s, Infected: %s, Confirmed: %s" % \
(self._round, len(self.healthy), len(self.infected), len(self.confirmed))
plt.text(-200, 400, t, ha='left', wrap=True)
实际效果
启动。
if __name__ == '__main__':
np.random.seed(0)
plt.figure(figsize=(16, 16), dpi=100)
plt.ion()
p = People(5000, 3)
for i in range(100):
p.update()
p.report()
plt.pause(.1)
plt.pause(3)
因为这个小 demo 主要是个人用来练手,目前一些参数没有完全抽出来。有需要的只能直接改源码。
后记
从多次实验的结果,通过调整人员的流动意愿,流动距离等因素,是可以得到直观的结论的。
本人也是初次使用 numpy
和 matplotlib
,现学现卖,若有使用不当之处请指正。其中的概率参数设置 基本没有科学依据,仅供 Python 爱好者参考。
总得来说,用 numpy
来模拟病毒感染情况应该是能行得通的。但是其中的影响因子还需要仔细设计。性能也是需要考量的问题。
愿疫情能早日过去,武汉加油,中国加油
尝试用 Python 写了个病毒传播模拟程序的更多相关文章
- 尝试用React写几个通用组件 - 带搜索功能的下拉列表,开关切换按钮,弹出框
尝试用React写几个通用组件 - 带搜索功能的下拉列表,开关切换按钮,弹出框 近期正在逐步摸索学习React的用法,尝试着写几个通用型的组件,整体项目还是根据webpack+react+css-me ...
- 十行代码--用python写一个USB病毒 (知乎 DeepWeaver)
昨天在上厕所的时候突发奇想,当你把usb插进去的时候,能不能自动执行usb上的程序.查了一下,发现只有windows上可以,具体的大家也可以搜索(搜索关键词usb autorun)到.但是,如果我想, ...
- 前端开发 | 尝试用Markdown写一下近几个月的总结
近期总结 回顾 半年前 半年前,接触了前端一年多(工作半年)的我了解的东西只有下面这些.因为在公司里的工作就是切静态页,捣鼓CMS. HTML (比较简洁的编写HTML) CSS/CSS3 (PC兼容 ...
- 尝试用python开发一款图片压缩工具1:尝试 pillow库
开发目的 我经常使用图片.公众号文章发文也好,还是生活中要使用素材.图片是一种比文字更加直观的载体.但是图片更加占用带宽,很多软件都对图片有大小限制.图片太大也会影响加载速度.我试过几款图片压缩工具, ...
- 尝试用canvas写小游戏
还是习惯直接开门见山,这个游戏是有一个老师抓作弊的学生,老师背身,点学生开始加分,老师会不定时回头,如果老师回头还在点学生在,就会被抓住,游戏game over. 1.写游戏首先是loading条,于 ...
- 怎么用Python写一个三体的气候模拟程序
首先声明一下,这个所谓的三体气候模拟程序还是很简单的,没有真的3D效果或数学模型之类的,只不过是一个文字表示的模拟程序.该程序的某些地方可能不太严谨,所以也请各位多多包涵. 所谓三体气候模拟,就是将太 ...
- python写service时全局变量问题
在尝试用flask写service的过程中,我发现全局变量使用虽然很方便,但其实是很冒险的. 本次我使用的是声明global变量的方式,如果作为本地的单次使用的程序来说,确实没有问题并且很好用,对于竞 ...
- 用Python写Verilog(非HLS)
https://blog.csdn.net/qq_32010099/article/details/81197171 前段时间玩Python的时候好奇, 既然Python这么强大, 那么能不能用Pyt ...
- 尝试用kotlin做一个app(二)
导航条 我想实现的效果是这样的 类似于ViewPager的效果,子类导航页面可以滑动,当滑动某个子类导航页面,导航线会平滑地向父类导航移动 ·添加布局 <!--导航分类:编程语言/技术文档/源码 ...
随机推荐
- 手摸手。完成一个H5 抽奖功能
要完成一个这样的抽奖功能 构思 奖励物品是通过接口获取的(img) 奖励结果是通过接口获取的(id) 抽奖的动画需要由慢到快再到慢 抽奖转动时间不能太短 抽奖结束需要回调 业务代码和功能代码要分离 先 ...
- ssh免密登陆和加密解密
一 丶实现无密码的远程管理 1.生成公钥 私钥 [root@room9pc14 桌面]# ssh-keygen [root@room9pc14 桌面]# ls /root/.ssh/ 2.上传公钥到虚 ...
- Git 合并多次提交
在合并分支的时候,希望将多次提交合并成一个,然后再 cherry-pick 到主分支. 合并分支 develop 分支做开发,可能会进行多次提交,但是在发布或者进行 PR 的时候,我们只希望看到一次提 ...
- Spring Boot2 系列教程 (七) | 使用 Spring Data JPA 访问 Mysql
前言 如题,今天介绍 Spring Data JPA 的使用. 什么是 Spring Data JPA 在介绍 Spring Data JPA 之前,首先介绍 Hibernate . Hibernat ...
- 关于Log4Net的使用及配置方式
目录 0.简介 1.安装程序包 2.配置文件示例 3.日记的级别:Level 4.日志的输出源:Appenders 5.日志格式:Layout 6.日志文件变换方式(回滚方式):RollingStyl ...
- MST + 树形 dp
Genghis Khan(成吉思汗)(1162-1227), also known by his birth name Temujin(铁木真) and temple name Taizu(元太祖), ...
- C++ 中的 unique 函数
unique 函数是用来去除一个集合中重复元素的函数 若是在数组中,则调用此函数后,返回的除去重复元素的下一个指针的地方 若是在 vector中,则会返回重复元素下一个位置的迭代器,在调用erase函 ...
- Java入门 - 语言基础 - 03.基础语法
原文地址:http://www.work100.net/training/java-basic-syntax.html 更多教程:光束云 - 免费课程 基础语法 序号 文内章节 视频 1 第一个Jav ...
- Java入门 - 高级教程 - 02.集合
原文地址:http://www.work100.net/training/java-collection.html 更多教程:光束云 - 免费课程 集合 序号 文内章节 视频 1 概述 2 集合接口 ...
- java8新特性Lambda和Stream
Java8出来已经4年,但还是有很多人用上了jdk8,但并没用到里面的新东西,那不就等于没用?jdk8有许多的新特性,详细可看下面脑图 我只讲两个最重要的特性Lambda和Stram,配合起来用可以极 ...