Stat110 - Introduction to Probability代码的R语言与Python实现

这篇文章主要是用于记录将哈佛大学Stat110的教材用书《Introduction to Probability》第一版一书中的R代码向Python进行翻译转换,相关资源链接:http://stat110.net

  本博文还在更新中,最新更新为27/04/2020, Chapter05,出于一些原因,暂时停更。建议你使用能够支持markdown的浏览器,如新版本chrome或edge来进行阅读,否则可能出现格式错乱问题。在完成以后会提供全文pdf文件。

  R语言在科学计算上的确有很多优势,但是学习起来学习曲线比较陡峭,不太适合新手。我用惯了Python,想要同时把他们两都操作一下熟悉熟悉。以后的代码对比都以下面的形式或者表格形式来展现。国内需要使用R的话,建议直接到清华镜像下载:https://mirrors.tuna.tsinghua.edu.cn/CRAN/

  注意:本博文只对书本上的代码进行介绍与翻译,对其相应的知识点只会粗略带过,需要读者自行进行学习。

Chapter 01

  对应原文1.8内容。

  第一章总结:

  第一章主要讲了一些基础的概率论知识,诸如:朴素概率定义(其实就是古典概型)和他的一些重复运算的解决方法,以及一些概率的性质而已。

  比较特别的是,第一单元里讲了一些Story Proof(故事论证)之类的东西让一些二项式系数公式的记忆变得尤为方便,而且里面举的很多例子都是很好帮助理解的内容,这是跟国内的教学差别很大的地方。

向量

Python:

v = np.array([3,1,4,1,5,9]) # 将v定义为一组向量[3,1,4,1,5,9]

R:

v <- c(3,1,4,1,5,9) # 将v定义为一组向量(3,1,4,1,5,9),c代表concatenate

  在Python里面此处用Numpy的数组做向量。与Python不同,R语言是建立在向量之上的,就算是简单的 n <- 100 都被R视为一个长度为1的向量。

  当然在R里面稍有不同的是,”=”与”<-“使用上稍有不同,在函数调用中,func(x=1)与func(x<-1)是有区别的,前者调用完后变量x不会被保留,而后者会在工作区里保留变量x=1。

  R与Python不同的地方还是有挺多的:

Python R 效果
sum(v) sum(v) 求和
max(v) max(v) 求最大值
min(v) min(v) 求最小值
len(v) length(v) 获得长度
range(m,n+1) m:n m,m+1,…n-1,n

  在元素获取上,两者区别还是挺大的,最大区别还是从0还是1开始数:

Python R 输出
v[0] v[1] 3
?(Python有吗) v[-(2:4)] 1 5 9
v[[0,2,4]] v[c(1,3,5)] [3,4,5] / 3 4 5
[x**(-2) for x in range(1,101)] 1/(1:100)^2 $(\frac{1}{1^2},\frac{1}{2^2},…,\frac{1}{100^2})$

阶乘与二项式

  在本来并不用于计算的Python中,运算这种需要调用别的库,例如阶乘:

Python:

import math
math.factorial(n)

R:

factorial(n)

  同样的也有像二项式,一般Python图方便会直接自己写一个函数,不过实在要调用也是可以的。

Python

from scipy.special import comb
comb(n,k)

R:

choose(n,k)

  R相较之下真的太方便了。此外R还有lchoose(n,k),lfactorial(n)用来运算log值,方便进行乘法转换加法。

取样模拟

  这方面二者用法是很相似的,直接上表格吧。

Python R 效果
np.random.choice(range(1,n+1),size=k,replace=False) sample(n,k) 从1-n中不放回抽样k次
np.random.choice(range(1,n+1),size=k) sample(n,k,replace=True) 从1-n中有放回抽样k次
np.random.choice([‘a’,’b’,…,’z’]),size=k,,replace=False) sample(letters,k) #letters是R自带的26个小写字母表数组 从26个字符中取不重复地取字母组成k字符单词
np.random.choice(range(1,5),size=3,replace=False,p=[0.1,0.2,0.3,0.4]) sample(4, 3, replace=TRUE, prob=c(0.1,0.2,0.3,0.4)) 1-4的概率分别为0.1,0.2,0.3,0.4

de Montmort‘s 配对问题

  de Montmort’s配对问题指的是一堆卡片标有1-n的数字,打乱后一张一张的掀开,第k次掀开的若对应的数字正好为k,那么则视为成功,求的是整套卡片翻完的至少成功一次的概率。

  单纯的运算需要运用Inclusion-exclusion公式:

de Montmort‘s

  而在代码里我们可以做simulation模拟。

Python:

n = 100
count = 0
for i in range(10**4):
    r = sum(np.random.choice(range(1,n+1),n,replace=False)==range(1,n+1))
    if r>=1:
        count+=1

print(count/10**4)

R:

n <- 100
r <- replicate(10^4,sum(sample(n)==(1:n))) # 重复10^4次,计算n个不重复取样与1:n的相同个数总数
sum(r>=1)/10^4

  R是真的简短很多,不愧是数字科学专用语言…不过说真的,python的代码相比之下更加友好。

生日问题

  对于生日问题的概率计算,Python,所幸numpy也支持prod。

Python:

1-np.prod((365-k+1,365+1))/365**k

R的话有两种方式:

1-prod((365-k+1):365)/365^k # 1.直接计算
pbirthday(k) # 2.使用内置的函数

  还挺犯规的,生日问题都内置了。而且R还能用qbirthday(n)来计算达到n概率两人以上相同生日所需要的人数。而且对于这两个函数,还能加上coincident参数来指定特定多少人同一天生日,在这就不做示范了。

  生日问题的模拟的话是这样:

Python:

count = 0
for i in range(10**4):
    if Counter(np.random.choice(range(365),23)).most_common(1)[0][1]>1:
        count += 1
print(count/10**4)

R:

r <- replicate(10^4, max(tabulate(sample(1:365,23,replace=TRUE)))) # 重复10^4次,计算23次从365个数重复取样取得相同次数最多的个数
sum(r>=2)/10^4

Chapter 02

  对应原文2.10的内容

  第二章总结:

  这一章很细致的讲了条件概率的各种东西,包括事件独立性,贝叶斯,全概率公式,多条件概率,条件的选择(三门问题,递归问题,Gambler’s ruin)各种谬误(Prosecutor’s fallcay,Simpson’s paradox)。

  内容都十分深入浅出,有一说一,相比起国内大学蓝色那本概率统计真的细致太多了。

模拟频率派的理解

  这里用到书上的例子:

  一个家庭两小孩($A$和$B$),在 ($C_1$) 已知至少有一个是女孩 和 ($C_2$) 已知有一个姐姐 分别的条件下,求两人都是女孩的条件概率,设事件$S$为两人都是女孩。

  这道题实际上就是求$P(S|C_1)$和$P(S|C_2)$。且因为前者是$\frac{1}{3}$而后者为$\frac{1}{2}$,让人觉得这道题的答案比较反直觉,但是其实却很合理。频率派的理解就是用大量实验去叠,那只需要取很多(这里取了$10^5$)个样就行。

Python:

n = 10**5
childa = np.random.choice([1,2],n,True)
childb = np.random.choice([1,2],n,True)
a = (childa==1) # 小孩A是女孩
b = (childb==1) # 小孩B是女孩
n_c1 = sum(a|b) # 至少一个女孩
n_s = sum(a&b) # A和B都是女孩
n_c2 = sum(a) # A是女孩
p_1 = n_s/n_c1 # P(S|C1)
p_2 = n_s/n_c2 # P(S|C2)
print('P(S|C1)=%f,P(S|C2)=%f' % (p_1,p_2))

R:

n <- 10^5
childa <- sample(2,n,replace=TRUE)
childb <- sample(2,n,replace=TRUE)
n.b <- sum(childa==1) # A是女孩
n.b2 <- sum(childa==1 | childb==1) # 至少一个女孩
n.ab <- sum(childa==1 & childb==1) # A和B都是女孩
n.ab/n.b # P(S|C2)
n.ab/n.b2 # P(S|C1)

  得到两个值一个接近0.33,另一个接近0.5,模拟完成。

三门问题模拟

  三门问题又称Monty Hall问题,比较经典的一个条件概率问题,在这里就不介绍了上网可以查到,可能之后会补充。先放一个书上的R语言版本,他加了一点交互式来进行模拟游戏:

monty <- function() {
    doors <- 1:3

    # randomly pick where the car is
    cardoor <- sample(doors,1)

    # prompt player
    print("Monty Hall says ‘Pick a door, any door!’")

    # receive the player’s choice of door (should be 1,2, or 3)
    chosen <- scan(what = integer(), nlines = 1, quiet = TRUE)

    # pick Monty’s door (can’t be the player’s door or the car door)
    if (chosen != cardoor) montydoor <- doors[-c(chosen, cardoor)]
    else montydoor <- sample(doors[-chosen],1)

    # find out whether the player wants to switch doors
    print(paste("Monty opens door ", montydoor, "!", sep=""))
    print("Would you like to switch (y/n)?")
    reply <- scan(what = character(), nlines = 1, quiet = TRUE)

    # interpret what player wrote as "yes" if it starts with "y"
    if (substr(reply,1,1) == "y") chosen <- doors[-c(chosen,montydoor)]

    # announce the result of the game!
    if (chosen == cardoor) print("You won!")
    else print("You lost!")
}

  R的代码用来写这种程序实在是…有点尴尬,交互和代码可读性过于差了。我的话,是尝试利用频率派的方式模拟,来计算多次三门问题分别在使用了 策略1: 一直换门 策略2:绝不换门 之下的概率,感觉这样对比更有实际意义。

Python:

import random

def MontyHall(n_select, is_change):
    n_car = random.randint(1, 3)
    if n_select == n_car:
        if is_change:
            return False
        else:
            return True
    else:
        if is_change:
            return True
        else:
            return False

# strategy of always changing doors    
count = 0
for i in range(10**5):
    n_select = random.randint(1, 3)
    if MontyHall(n_select, True):
        count += 1
print('Prob of changed', count/10**5)

# Strategy of always NOT changing doors   
count = 0
for i in range(10**5):
    n_select = random.randint(1, 3)
    if MontyHall(n_select, False):
        count += 1
print('Prob of not changed', count/10**5)

R:

monty <- function(n_selected, is_change) {
    n_car <- sample(3,1)
    if(is_change){
        if(n_car == n_selected){
            return(FALSE)
        }else{
            return(TRUE)
        }
    }
    else{
        if(n_car == n_selected){
            return(TRUE)
        }else{
            return(FALSE)
        }
    }
}

# Strategy of always changing doors
r <- replicate(n=10^4,monty(sample(3,1),TRUE))
sum(r/10^4)

# Strategy of always NOT changing doors
r <- replicate(n=10^4,monty(sample(3,1),FALSE))
sum(r/10^4)

  得到两个值一个接近0.33,另一个接近0.66,完事了。

  不过从这代码啊就可以看出来,Python相较于R还是更加万金油一点,Python写循环写嵌套都是比较清晰的,R这个语法就写这么个小函数却实在把用惯Java,Python之类语言的我糊弄的难顶。(扶额)

Chapter 03

  对应原文3.11内容。

  第三章总结:

  第三章开始讲随机变量了,这一章和下一章好像都是只讲离散型。

  这一章只讲了几个常见离散分布,除了Bernoulli和Binomial以外,还有一个在国内大学概率论好像不接触,叫超几何分布HyperGeometric,虽然听的挺多但是正式去了解还是第一次。

  印象中除了这些分布以外,还有他讲的内容也很特别。例子有很多,包括PMF,CDF的定义证明之类的,上面三个分布的联系之类的,不过让我印象最深刻的还是他给随机变量与分布关系的定义:

分布之于随机变量,就像蓝图之于房子。不同的房子可以用同样的蓝图,而不同的随机变量也能有相同的分布。

  书本内容真的很深入浅出,不过继续代码吧。(本章代码省略了离散型均匀分布部分,那部分连Stat110上课都没讲,因为无非就是古典概型取样,可以参照第一章内容)

二项分布

  贝努利分布就没必要整了,就是一次二项分布。以在$X\sim{Bin(5,0.2)}$当$X=3$为例子。

  (国内一般用的标识应该是$X\sim{B(5,0.2)}$)

Python:

# Python需要用到scipy的stats模块
from scipy.stats import binom
print('P(X=3):',binom.pmf(3,5,0.2),'\nF(X=3):', binom.cdf(3,5,0.2))
print('Samples of X~Bin(5,0.2)',binom.rvs(size=7,n=5,p=0.2))

# 输出:
> P(X=3): 0.051200000000000016 
> F(X=3): 0.99328
> array([2, 1, 1, 1, 1, 2, 0])

R:

dbinom(3,5,0.2) # PMF
pbinom(3,5,0.2) # CDF
rbinom(7,5,0.2) # 从分布中随机取样7个

#输出
[1] 0.0512
[2] 0.99328
[3] 1 2 1 2 3 2 2

  很显然,R的代码辨识度是真的不好。相比之下Python用户体验还是很好的,难怪支持者两极分化严重。

超几何分布

  废话不多说,对于PMF,CDF和取样的做法大同小异,要不是书上有我也不写了,直接上表格算了。

  超几何分布标识一般是这样$X\sim{HGeom(w,b,n)}$,不过也有是这样标识的$X\sim{G(n,M,N)}$,这两种标识正好代表了在R和在Python里的不同用法。(R是用第一个,而Python是第二个)

  先简单的说明一下,超几何分布其实就是不放回取样的成功数。而$X\sim{HGeom(w,b,n)}$里,$w$代表了所有样本中成功的样本数,$b$代表所有样本中失败的样本数,$n$代表取样的数目。而在标识$X\sim{G(n,M,N)}$里,$n$还是代表取样本数,而$M$代表所有样本数,$N$表示成功的样本数。

  的确有所区别,不过大同小异,Python的话记得import调包就完事了。

Python R
hypergeom.pmf(k=5,n=8,M=30,N=10) dhyper(5,10,20,8)
hypergeom.cdf(k=5,n=8,M=30,N=10) phyper(5,10,20,8)
hypergeom.rvs(size=10,n=8,M=30,N=10) rhyper(10,10,20,8)

Chapter 04

  对应原文4.11内容。

  第四章总结:

  第四章整的东西还挺多,把全部常用的离散型分布给讲完了,而且还挺多的,包括几何分布、第一次成功分布、负二项分布和泊松分布,还有诸如期望、方差的定义与性质。这一章最有意思的还是一个随机变量标识器(Indicator r.v.s)的思想,在分析的时候尤其是对期望分析的时候特别有用。

几何分布、负二项分布、泊松分布

  因为第一次成功分布(First Success Distribution)就是几何分布的随机变量加一,没什么好说的,就先把这三个简单罗列一下吧。Python同样使用的是Scipy.stats模块。

Python R Distribution
geom.pmf/cdf/rvs d/p/rgeom $X\sim{Geom(p)}$
nbinom.pmf/cdf/rvs d/p/rnbinom $X\sim{NBin(r,p)}$
possion.pmf/cdf/rvs d/p/rpois $X\sim{Pois(\lambda{})}$

求期望

  书上是直接用了第一章Matching Problem的例子来求期望。按道理这里的期望模拟得到会十分接近于 1,至于为什么有兴趣的各位请看书吧,在Example 4.4.4上利用了随机变量标识器来进行计算得到的。

Python:

n = 100
count = 0
r = []
for i in range(10**4):
    r.append(sum(np.random.choice(range(1,n+1),n,replace=False)==range(1,n+1)))
print(np.mean(r))

R:

n <- 100
r <- replicate(10^4,sum(sample(n)==(1:n)))
mean(r)

另一个生日问题模拟

  这一个生日问题来自Example 4.4.5,也是一个随机变量标识器的例子。意思是有n个人,求365天里至少有一人生日的日子数的期望值。这个值会无限接近于$365(1-(\frac{364}{365})^n)$,这里例子取$n=20$。

Python:

import numpy as np
k = 20
r = []
for i in range(10**4):
    r.append(len(np.unique(np.random.choice(range(1,365+1),k,True))))
print(np.mean(r))

R:

k <- 20
r <- replilcate(10^4, {bdays <- sample(365,k,replace=TRUE); length(unique(bdays))})
mean(r)

Chapter 05

  对应原文5.9内容。

  第五章总结:

  第五章完成了基本上是这个课程的一半了,内容还不少。虽然这篇博文注重代码,但还是把一个很重要的表格(见下图)放上来一下。这一章讲的是连续型随机变量以及各种连续型分布,包括均匀分布、Logistic分布、Rayleigh分布、正态分布与指数分布,还稍微提了提泊松过程。

fff

  这一章比较重点的内容是:位置-刻度转换思想(Location-scale transformation),均匀分布的普适性(Universality of the Uniform)

均匀分布、正态分布、指数分布等

  把这几个简单罗列一下吧。Python同样使用的是Scipy.stats模块。在命名上和上一次的超几何分布的参数上就可以看出来R语言很倾向数学上的命名法,不过暂时我没找到R写的Rayleigh分布。

Python R Distribution
uniform.pmf/cdf/rvs d/p/runif $X\sim{Unif(a,b)}$
norm.pmf/cdf/rvs d/p/rnorm $X\sim{N(\mu,\sigma^2)}$
expon.pmf/cdf/rvs d/p/rexp $X\sim{Expo(\lambda)}$
logistic.pmf/cdf/rvs d/p/rlogis $X\sim{Logistic}$
rayleigh.pmf/cdf/rvs ? $X\sim{Rayleigh}$

  顺便在这里用正态分布表演一个location-scale变换。

  由$\frac{X-\mu}{\sigma}\sim{N(0,1)}$来获得$X\sim{N(\mu,\sigma^2)}$,此处$\mu=1$,$\sigma=2$。

Python:

import matplotlib.pyplot as plt
mu = 1
sigma = 2
x = mu + sigma * norm.rvs(size=10**4)
plt.hist(x) # 效果如下图

fff

R:

mu <- 1
sigma <- 2
x <- mu + sigma * rnorm(10^4)
hist(x) # 效果如下图

fff

  R和Python在这方面上几乎完全一致,不用怎么翻译就很舒服。

画曲线

  再用标准正态分布在[-3,3]上的图整个pdf。

Python:

x = np.arange(-3,3,0.01)
y = norm.pdf(x)
plt.plot(x,y) # 效果如下图

fff

R有两种方式:

# 第一种跟Python一样
x <- seq(-3,3,0.01)
y <- dnorm(x)
plot(x,y,type='l')

# 第二种:R独有的
curve(dnorm, from=-3,to=3,n=1000) # 效果如下图

fff

均匀分布的普适性模拟

  这里用Logistic分布来实现普适性的一个效果,即$log(\frac{U}{1-U})\sim{Logistic}$。

Python:

import numpy as np
from scipy.stats import uniform
from scipy.stats import logistic
import matplotlib.pyplot as plt

plt.figure(figsize=(13, 5)) #调整图像大小

# 使用普适性
u = uniform.rvs(size=10**4)
x = np.log(u/(1-u))
plt.subplot(1,2,1)
plt.hist(x,bins=20)

# 直接获取分布
plt.subplot(1,2,2)
x = np.arange(-10,10,0.01)
y = logistic.pdf(x)
plt.plot(x,y)
plt.show()

fff

R:

# 使用普适性
u <- runif(10^4)
x <- log(u/(1-u))
hist(x)

# 直接获取分布
curve(dlogis, from=-15,to=15,n=1000)

fff

  反正就是输出跟直接获取分布走势很像,就模拟认为他可行了。

泊松过程模拟与验证

  指数分布与泊松分布有联系。指数分布每一个值是发生下一次无记忆性事件的时间点,所以是连续的。泊松分布可以视作单位时间内发生事件的次数。那么,对于rate为$\lambda$的泊松过程,会有,

  • 间隔为1的时间段内发生事件的次数服从$Pois(\lambda)$

  • 每次事件发生的时间间隔服从$Expo(\lambda)$

  下面来用指数分布生成时间间隔的方式验证”时间段内次数服从泊松分布”:

Python:

import numpy as np
from scipy.stats import expon
from scipy.stats import poisson
import matplotlib.pyplot as plt

plt.figure(figsize=(13, 5)) #调整图像大小

# 泊松过程
lamb = 10
r = []
for i in range(10**4):
    r.append(sum(np.cumsum(expon.rvs(size=20,scale=1/lamb))<1))
plt.subplot(1,2,1)
plt.hist(r,bins=20)

# 泊松分布
x = list(range(21))
y = poisson.pmf(x,mu=lamb)
plt.subplot(1,2,2)
plt.plot(x,y)
plt.show()

fff

R:

# 泊松过程
lambda = 10
r = replicate(10^4,sum(cumsum(rexp(20,lambda))<1))
hist(r)

# 泊松分布
x <- 0:20
y <- dnorm(x,lambda)
plot(x,y,type='l')

fff

  图像都接近,完事了。

  要注意的是,对于Python与R的pmf函数,pdf函数之类的,参数都不太一样,所以必须要看清楚再输入。举这里的例子:

  在Python中,指数分布的pdf/cdf/rvs调整参数是通过location-scale转换来调整的,这是他的参数:expon.pdf(x,loc=0,scale=1)

  所以我为了获得$\lambda=10$的指数分布,输入了scale=1/10。

  而在R中,参数是这样的dexp(k,rate=1),那么直接输入rate=10即可。

  同理,对于泊松分布,Python里要输入参数的也不是$\lambda$,而是$\mu=\lambda t$。所以在这里我们取单位时间1,这时$\mu=\lambda$。而R中直接就是输入$\lambda$的值作为参数即可。二者区别还是很大的,总而言之还是R偏向数学用语。

  对于泊松过程详细内容请尽量参考书本或别的资料,在此不会详细解释。


上一篇
下一篇