第五章 编程挑战

一、使用文氏图来可视化集合之间的关系

文氏图(Venn diagram)是指通过图形来观察集合之间关系的简单方法,他可以告诉我们两个集合之间有多少共同的元素,有多少元素只在其中一个集合中。考率一个集合A,它表示小于20的正的奇数的集合,即A = {1,3,5,7,9,11,13,15,17,19},考虑另一个集合B,它表示小于29的质数,即B = {2,3,5,7,11,13,17,19}。我们可以在Python中使用matplotlib_venn包绘制文氏图,安装好matplotlib_venn包后(以pycharm为例,在终端Terminal输入 pip install matplotlibv_venn,即可完成安装)就可以如下绘制文氏图:


 '''
 Draw a Venn diagram for two sets
 '''
 
 from matplotlib_venn import venn2
 import matplotlib.pyplot as plt
 from sympy import FiniteSet
 
 def draw_venn(sets):
     venn2(subsets=sets)
     plt.show()
 if __name__ == '__main__':
     s1 = FiniteSet(1, 3, 5, 7, 9, 11, 13, 15, 17, 19)
     s2 = FiniteSet(2, 3, 5, 7, 11, 13, 17, 19)
     draw_venn([s1, s2])

导入我们所需要的模块、函数和类(venn()函数、matplotlib.pyplot和FiniteSet类)之后,我们要做的就是创建两个集合,并调用venn2()函数,使用subsets关键词参数将集合指定为元组。

下图显示了使用上述程序创建的文氏图。集合A和B共享了7个公共元素,所以数字7显示在公共区域内。每一个集合也有自己独特的元素,即3和1分别显示在各自的区域内。图中集合下方的标签显示为集合符号A和B,你也可以使用set_labels关键字参数设定自己想要的标签。


 venn2(subsets=sets, set_labels=('S', 'T'))

这个命令将集合A和B的标签设定为S和T。

你可以自己挑战一下,假设你已经创建了一个在线调查问卷(询问你的同学),问卷中包括以下问题:你踢足球、还是做其他运动,或是不做运动?获得结果后,创建一个CSV文档sports.csv,如下:


 StudentID,Football,Others
 1,1,0
 2,1,1
 3,0,1
 --snip--

为班里的20名同学创建20个这样的行,第一列是学生ID(不匿名),如果学生选择了“足球”作为其爱好的运动,则第二列相应位置填1,如果学生选择其他任何运动或者不做运动,则在第三列相应位置填1。编写一个程序来创建一个文氏图,以总结该调查的结果,如下图所示。

根据创建的sports.csv文档中的数据,每一个集合中的元素数量可能会有不同。下面的函数可用来读取一个csv文档并返回两个列表,分别是爱好足球和其他运动学生的ID:


 def read_csv(filename):
     football = []
     others = []
     with open(filename) as f:
         reader = csv.reader(f)
         next(reader)
         for row in reader:
             if row[1] == '1':
                 football.append(row[0])
             if row[2] == '1':
                 others.append(row[0])
     return football, others

完整代码如下:


 from matplotlib_venn import venn2
 import matplotlib.pyplot as plt
 from sympy import FiniteSet
 import csv
 def read_csv(filename):
     football = []
     others = []
     with open(filename) as f:
         reader = csv.reader(f)
         next(reader)
         for row in reader:
             if row[1] == '1':
                 football.append(row[0])
             if row[2] == '1':
                 others.append(row[0])
     return football, others
 
 def draw_venn(sets):
     venn2(subsets=sets, set_labels=('Football', 'Others'))
     plt.show()
 if __name__ == '__main__':
     football, others = read_csv('sports.csv')
     football = FiniteSet(*football)
     others = FiniteSet(*others)
     draw_venn([football, others])

二、大数定律

我们之前用到了掷骰子和掷硬币这两个例子,例子中的随机事件可以用随机数来进行模拟。我们用事件来表示掷骰子时掷出的具体数字,或掷硬币时出现的正面或反面,每一个时间对应一个相关的概率值。在概率论中,一个随机变量(常用X表示)可用来描述随机事件。例如,X = 1描述了掷一次骰子出现数字1的时间,用P(X=1)表示相应的概率。用两种类型的随机变量:离散型随机变量,他只去整数值,是我们在这章中看到的唯一一种随机变量:连续性随机变量,顾名思义,它可以取任意实数值。

离散型随机变量的期望E即为我们在第三章学习的平均值或均值,期望可以通过下式计算:


因此,对于一个6面骰子,掷一次骰子的期望值可以按如下方式计算:


 >>> e = 1*(1/6) + 2*(1/6) + 3*(1/6) + 4*(1/6) + 5*(1/6) + 6*(1/6)
 >>> e
 3.5

根据大数定律,随着试验次数的增加,多次实验结果的平均值会越来越接近期望值。这一任务的挑战就是,分别在下列试验次数为100、1000、10000、100000和500000的情况下投掷6面骰子来验证大数定律的准确性。一下是你程序的一种可能运行结果:


 Expected value : 3.5
 Trials: 100 Trial average 3.66
 Trials: 1000 Trial average 3.471
 Trials: 10000 Trial average 3.4954
 Trials: 100000 Trial average 3.50081
 Trials: 500000 Trial average 3.503704

代码实现:


 from sympy import FiniteSet
 import random
 def return_randint():
     return random.randint(1, 6)
 def calculate_average(trials):
     sum = 0
     for i in range(1, trials+1):
         sum += return_randint()
     return sum / trials
 if __name__ == '__main__':
     expect = 1*(1/6) + 2*(1/6) + 3*(1/6) + 4*(1/6) + 5*(1/6) + 6*(1/6)
     print('Expected value : {0}'.format(expect))
     trials_num_list = [100, 1000, 10000, 100000, 500000]
     for i in trials_num_list:
         average = calculate_average(i)
         print('Trials: {0} Trial average {1}'.format(i, average))

三、掷多少次硬币会输光你的钱?

我们来考虑一个简单的掷均匀硬币的游戏。一个玩家在掷出正面时赢1美元。掷出反面时输1.5美元,当玩家的账户余额为0时结束游戏。由使用者输入一个特定的起始数额值,你的挑战是编写一个模拟这个游戏的程序。假设你对手(计算机)的账户无金额限制。以下是一个可能的游戏过程:


 Enter your  starting amount: 2
 Heads! Current amount: 3.0
 Tails Current amount: 1.5
 Tails Current amount: 0.0
 Game over :(Current amount :0.0. Coin tosses: 3

代码实现:


 import random
 
 
 def return_randint():
     r = random.random()
     if r >= 0.5:
         return 1
     else:
         return 0
 def loss_all_money_game(money):
     count = 0
     while money > 0:
         r = return_randint()
         if r == 1:
             money -= 1.5
             print('Tails Current amount: {0}'.format(money))
             count += 1
         else:
             money += 1
             print('Heads! Current amount: {0}'.format(money))
             count += 1
     print('Game over :(Current amount :{0}. Coin tosses: {1}'.format(money, count))
 
 
 if __name__ == '__main__':
     start_money = float(input('Enter your  starting amount: '))
     loss_all_money_game(start_money)

四、洗牌

考虑一副标准的扑克牌(52张)。你的挑战是写一个程序模拟洗牌。简单起见,建议你使用整数1,2,3,...,52来表示这副牌。每次运行这个程序,它都会输出一组洗过的牌,在本例中,输出的是一个无序排列的整数列表。

一下是你程序的可能输出:


 [29, 10, 40, 15, 16, 26, 42, 32, 19, 38, 35, 18, 11, 36, 12, 33, 51, 28, 43, 22, 30, 48, 4, 50, 17, 21, 45, 49, 31, 5, 52, 27, 20, 2, 1, 8, 25, 46, 6, 13, 47, 9, 3, 41, 44, 14, 23, 39, 34, 7, 24, 37]

Python标准库中的random模块中有一个shuffle()函数,可以准确地实现这个操作:


 >>> import random
 >>> x = [1,2,3,4]
 >>> random.shuffle(x)
 >>> x
 [4, 1, 2, 3]

创建一个列表x,包含数字[1,2,3,4]。然后调用shuffle()函数,将此列表作为参数输入,你将看到x中的数字顺序被打乱了。

但是如何人在扑克牌中使用这个程序呢?这里,仅仅输出无序的整数列表是不够的,你还需要一个方法把这些整数还原成能认出具体花色以及大小的牌。方法之一是创建一个Python类来表示一张具体的牌:


 class Crad:
     def __init__(self, suit, rank):
         self.suit = suit
         self.rank = rank

为表示梅花A,创建这张牌的对象,card1 = Crad('clubs','ace'),然后,对所有其他牌都按照这样操作。接着,创建一个由每张牌的对象组成的列表,并无序排列这个列表。结果是洗过的并能认出花色和大小的一副牌。程序的输出应该类似于如下内容:


 Ace of Spades
 2 of Hearts
 3 of Spades
 Ace of Hearts
 10 of Clubs
 3 of Hearts

代码实现:


 import random
 class Crad:
     def __init__(self, suite, rank):
         self.suite = suite
         self.rank = rank
 def initalize_deck():
     suites = ['Clubs','Diamond', 'Hearts', 'Spades']
     ranks = ['Ace', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen','King']
     cards = []
     for suite in suites:
         for rank in ranks:
             card = Crad(suite, rank)
             cards.append(card)
     return cards
 def shuffle_and_print(cards):
     random.shuffle(cards)
     for card in cards:
         print('{0} of {1}'.format(card.rank, card.suite))
 if __name__ == '__main__':
     cards  =initalize_deck()
     shuffle_and_print(cards)

五、估计一个圆的面积

考虑一个飞镖的靶盘,其外形是边长为2r的正方形内部嵌着一个半径为r的圆。现在我们将开始朝靶子掷大量的飞镖,其中有些飞镖;落在圆的内部,假设有N个,其他飞镖将落在圆的外面,假设由M个。如果我们考虑落在圆内部的飞镖的比例:


那么值f*A,此处A是正方形的面积,将大致等于圆的面积。在途中飞镖用小圆点表示。我么将f*的值作为圆的面积的估计值,实际的面积当然是\pi r^2。

作为这个挑战的一部分,用上述方法编写一个程序,在给定半径的情况下,估算出圆的面积。程序应分别在给定的掷飞镖数目(10^3,10^4,10^5)下输出估算的面积。这么多的飞镖!你将看到飞镖数量的增大使得估算的圆的面积更加接近于圆的真实面积。以下是运行结束时的示例输出:


 Enter radius = 1
 Area: 0.774 , Estimated ( 1000 darts ): 0.77400 , pi is 3.096
 Area: 0.7862 , Estimated ( 10000 darts ): 0.78620 , pi is 3.1448
 Area: 0.78333 , Estimated ( 100000 darts ): 0.78333 , pi is 3.13332

掷飞镖可以通过调用random.uniform(a, b)函数来模拟,该函数将返回一个介于a与b之间的随机数。在本例中,分别设置a =0和b = 2r(正方形边长)。

估计Π值

再次考虑上图。正方形的面积是4r^2,内嵌的圆的面积是\pi r^2。如果我们用圆的面积除以正方形的面积,则结果是Π/4。前面已经计算过比值f,即


可以看作Π/4的近似值,反过来也意味着


接近于Π值。你的下一个挑战是编写一个程序来估计Π的值(假设任意给定的半径值)。当增加飞镖的数量是,Π的估计值应该会接近于已知的常数Π。

代码实现:


 
 import random
 def calculate_count(num, r):
     N = 0
     M = 0
     for i in range(1, num+1):
         x = random.uniform(0, r)
         y = random.uniform(0, r)
 
         if (x ** 2 + y ** 2) ** 0.5 <= r:
             N += 1
         else:
             M += 1
     return N/(N+M) * r ** 2, 4*N /(N+M)
 
 
 if __name__ == '__main__':
     seconds = [10**3, 10**4, 10**5]
     r = int(input('Enter radius = '))
     for second in  seconds:
         area, pi = calculate_count(second, r)
         print('Area: {0} , Estimated ( {1} darts ): {2:.5f} , pi is {3}'.format(area, second, area, pi))

值得一讲的是,代码实现思路为把整个圆简化为1/4圆,故正方形设定范围为边长为r,此时我们可判断飞镖在的范围内的计数N+1,在其他范围上即M+1,此时以为1/4圆的面积近似值,的近似值。



发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章