Γλώσσα Προγραμματισμού ΙΙ

Διάλεξη 26ης Μαρτίου 2015

Συνεχίζουμε τη συζήτηση για την βιβλιοθήκη NumPy με περισσότερες λεπτομέρεις για τις συναρτήσεις της NumPy που παράγουν τυχαίους αριθμούς.

Τυχαίοι αριθμοί

Αρκετή από την αρχική δουλειά στην θεωρία των πιθανοτήτων αφιερώθηκε στην ανάλυση τυχερών παιχνιδιών, κυρίως παιχνιδιών με ζάρια. Το ενδιαφέρoν ενός εκ των πρωτοπόρων της θεωρίας των πιθανοτήτων, του Γάλλου μαθηματικού Blaise Pascal, κινήθηκε από την ερώτηση ενός φίλου κατά πόσο θα ήταν επικερδές να στοιχηματίσει ότι δεν θα φέρει εξάρες αν ρίξει ένας ζεύγος από ζάρια 24 φορές. Σήμερα γνωρίζουμε ότι η πιθανότητα να μην εμφανιστούν εξάρες είναι (35/36)24, δηλαδή περίπου 0.51. Μπορούμε να γράψουμε ένα πικρό πρόγραμμα για να προσομειώσουμε το συγκεκριμένο παιχνίδι και να προσεγγίσουμε πειραματικά την παραπάνω πιθανότητα.


def rollDie():
	return np.random.random_integers(1,6)

def playGame(numTrials):
	yes = 0.0
	for i in range(numTrials):
		for j in range(24):
			d1 = rollDie()
			d2 = rollDie()
			if d1 == 6 and d2 == 6:
				yes += 1
				break
	print 'Probability of losing = ' + str(1.0 - yes / numTrials)

Αν καλέσουμε τη συνάρτηση playGame με όρισμα, 100000, 200000, 400000 και 800000 παίρνουμε, αντίστοιχα, τα αποτελέσματα


Probability of losing = 0.50897
Probability of losing = 0.50998
Probability of losing = 0.508605
Probability of losing = 0.5088375

ενώ η τιμή της έκφρασης (35/36)24 είναι κατά προσέγγιση 0.5085961238690966. Δοκιμάστε και εσείς να προσομοιώσετε το παραπάνω παιχνίδι. Σημειώνουμε ότι η κλήση random_integers(a,b) επιστρέφει ένα τυχαίο αριθμό στο διάστημα [a,b]. Άλλες χρήσιμες συναρτήσεις από το πακέτο random της βιβλιοθήκης NumPy είναι:

Ως ένα τελευταίο παράδειγμα, θα υπολογίσουμε προσεγγιστικά την πιθανότητα να έχουμε το λεγόμενο "χρώμα", στα αγγλικά flush, δηλαδή πέντε φύλλα του ιδίου χρώματος, σε ένα παιχνίδι poker. Μια τράπουλα αποτελείται από 52 χαρτιά χωρισμένα σε τέσσερα χρώματα, τις κούπες (hearts), τα μπαστούνια (clubs), τα καρώ (diamonds) και σπαθιά (spades). Μπορούμε επομένως να φτιάξουμε εύκολα μια τράπουλα με την παρακάτω συνάρτηση:


def make_deck():
	suits = ['C', 'D', 'H', 'S']
	ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
	deck = [s+r for s in suits for r in ranks]
	np.random.shuffle(deck)                        # ανακάτεμα της τράπουλας
	return deck

Αν θέλουμε να μοιράσουμε πέντε φύλλα από την τράπουλα, αρκεί να δώσουμε σε κάθε παίκτη πέντε φύλλα, για παρέδειγμα με την εντολή hand = deck[:5]. Θα χρειαστούμε επίσης και ένα τρόπο να διαπιστώσουμε αν τα χαρτιά τα οποία πήραμε είναι όλα του ιδίου χρώματος:


def same_suit(hand):
	suits = [card[0] for card in hand]
	count = {}
	for s in suits:
		n = suits.count(s)
		if n > 1:
			count[s] = n
	return count

Για να υπολογίσουμε τώρα την πιθανότητα να έχουμε "χρώμα" μπορούμε να ανακατέψουμε την τράπουλα, να μοιράσουμε πέντε φύλλα και να μετρήσουμε πόσα χέρια περιέχουν "χρώμα". Ο λόγος αυτού του αριθμού πρός τον συνολικό αριθμό που μοιράσαμε τα πέντε φύλλα θα είναι κοντά στην πραγματική πιθανότητα. Δεν είναι δύσκολο να υπολογίσει κανείς ότι η πιθανότητα χρώματος είναι ακριβώς 5148/2598960, δηλαδή περίπου 0.0019807923169267707.


# Shuffle the deck N times

N = 900000

flush = 0

for i in range(N):
	deck = make_deck()
	hand = deck[:5]

#	Count hands with five cards of the same suit (flush)

	count = same_suit(hand)
	if 5 in count.values():
		flush += 1

# Approximate probabilities

print 'Probability of a flush:', flush/float(N), 'Exact probability:', 5148/2598960.0

Πολυώνυμα

Η βιβλιοθήκη NumPy παρέχει το πακέτο numpy.polynomial για την αναπαράσταση πολυωνύμων, όπως και πλήθος συναρτήσεων που δρούν πάνω σε αυτά. Η αναπαράσταη ενός πολυωνύμου γίνεται με την αναγραφή των συντελεστών του από το σταθερό όρο προς τον μεγιστοβάθμιο όρο σε μια λίστα:


from numpy.polynomial import Polynomial as P
>>> p = P([1,2,3])
>>> p
Polynomial([ 1.,  2.,  3.], [-1.,  1.], [-1.,  1.])
>>> p.coef
array([ 1.,  2.,  3.])

Σημειώνουμε ότι η πρώτη γραμμή κώδικα εισάγει το όνομα P ως συντόμευση του ονόματος Polynomial. Διαφορετικά, η 2η γραμμή που κατασκευάζει το πολυώνυμο p θα έπρεπε να γραφεί ως p = np.polynomial.Polynomial([1, 2, 3]). Το πολυώνυμο στο οποίο αναφερόμαστε εδώ είναι φυσικά το $p(x) = 1 + 2x + 3x^2$. Προς το παρόν δεν θα ασχοληθούμε με τις λίστες [-1., 1.] και [-1., 1.] που επιστρέφει η συνάρτηση Polynomial. Πράξεις μεταξύ πολυωνύμων μπορούν να γίνουν με τον αναμενόμενο τρόπο:


>>> p + p
Polynomial([ 2.,  4.,  6.], [-1.,  1.], [-1.,  1.])
>>> p - p
Polynomial([ 0.], [-1.,  1.], [-1.,  1.])
>>> p * p
Polynomial([  1.,   4.,  10.,  12.,   9.], [-1.,  1.], [-1.,  1.])
>>> p**2
Polynomial([  1.,   4.,  10.,  12.,   9.], [-1.,  1.], [-1.,  1.])
>>> p / 2
Polynomial([ 0.5,  1. ,  1.5], [-1.,  1.], [-1.,  1.])
>>> p // P([-1, 1])
Polynomial([ 5.,  3.], [-1.,  1.], [-1.,  1.])
>>> p % P([-1, 1])
Polynomial([ 6.], [-1.,  1.], [-1.,  1.])

Σημειώνουμε ότι για την διαίρεση πολυωνύμων θα μπορούσαμε να είχαμε γράψει:


>>> quo, rem = divmod(p, P([-1, 1]))
>>> quo
Polynomial([ 5.,  3.], [-1.,  1.], [-1.,  1.])
>>> rem
Polynomial([ 6.], [-1.,  1.], [-1.,  1.])

Για να υπολογίσουμε την τιμή ενός πολυωνύμου σε ένα ή περισσότερα σημεία αρκεί να δώσουμε το ή τα σημεία μέσα σε παρενθέσεις μετά το όνομα του πολυωνύμου. Οι ρίζες του πολυωνύμου μπορούν να βρεθούν με τη μέθοδο roots():


>>> x = np.arange(5)
>>> p(x)
array([  1.,   6.,  17.,  34.,  57.])
>>> p.roots()
array([-0.33333333-0.47140452j, -0.33333333+0.47140452j])

Τόσο ολοκλήρωση όσο και παραγώγιση πολυωνύμων υποστηρίζεται από συναρτήσεις του πακέτου numpy.polynomial:


>>> p = P([2, 6])
>>> p.integ()
Polynomial([ 0.,  2.,  3.], [-1.,  1.], [-1.,  1.])
>>> p.integ(2)
Polynomial([ 0.,  0.,  1.,  1.], [-1.,  1.], [-1.,  1.])

Παρατηρήστε ότι στο πρώτο παράδειγμα η σταθερά της ολοκλήρωσης είναι μηδέν ενώ στο δεύτερο παράδειγμα το πολυώνυμο $p(x) = 2 + 6x^2$ ολοκληρώνεται δυό φορές (με τη σταθερά ολοκλήρωσης να εξακολουθεί να είναι μηδέν). Η μέθοδος deriv() για την παραγώγιση ενός πολυωνύμου λειτουργεί με ανάλογο τρόπο:


>>> p = P([1, 2, 3])
>>> p.deriv(1)
Polynomial([ 2.,  6.], [-1.,  1.], [-1.,  1.])
>>> p.deriv(2)
Polynomial([ 6.], [-1.,  1.], [-1.,  1.])