2.1. NumPy#
Οι αριθμητικές μέθοδοι πολύ συχνά ενέχουν μαθηματικούς υπολογισμούς με πολυδιάστατους πίνακες. Για την υλοποίηση των αριθμητικών μεθόδων σε κώδικα Python θα μπορούσαμε να χρησιμοποιήσουμε λίστες και την μονάδα κώδικα math, αλλά αυτά τα εργαλεία δεν έχουν την εξειδίκευση και τις υπολογιστικές επιδόσεις που θα θέλαμε. H βιβλιοθήκη NumPy καλύπτει αυτό το κενό, παρέχοντας κατάλληλες δομές δεδομένων για πολυδιάστατους πίνακες και συναρτήσεις βελτιστοποιημένες για μαθηματικούς υπολογισμούς με αυτές.
2.1.1. Αριθμητικοί τύποι δεδομένων#
H NumPy έχει τους δικούς της βασικούς τύπους δεδομένων. Οι πιο συνηθισμένοι αριθμητικοί τύποι δίνονται στον Πίνακα 2.1.
Τύπος |
bit |
Εναλλακτικά ονόματα |
---|---|---|
int32 |
32 |
|
int64 |
64 |
|
float32 |
32 |
single |
float64 |
64 |
double |
Μπορείτε να ανατρέξετε στην τεκμηρίωση της NumPy για τον πλήρη κατάλογο.
Σε συστήματα αρχιτεκτονικής 64 bit χρησιμοποιούνται εξ ορισμού οι τύποι numpy.int64 και numpy.float64, εκτός κι αν δηλώσουμε ρητά διαφορετικό τύπο. Ο numpy.float64 χρησιμοποιείται συνηθέστερα στην αριθμητική ανάλυση και καλείται και διπλής ακρίβειας (numpy.double) σε αντιδιαστολή με τον numpy.float32 ή numpy.single που αποτελούσε τον κανόνα σε παλιότερα συστήματα 32 bit.
Important
Ο τύπος float της καθιερωμένης Python καταλαμβάνει κι αυτός 64 bit, αλλά έχει διαφορετική συμπεριφορά σε ειδικές περιπτώσεις. Κατά την διαίρεση ενός numpy.float64 με το μηδέν, η NumPy υπολογίζει μια ειδική τιμή με εξαγωγή προειδοποίησης (RuntimeWarning). Αντίθετα ο ίδιος υπολογισμός με float προκαλεί τερματισμό με εξαγωγή σφάλματος (ZeroDivisionError).
import numpy as np
a = np.double(1)
print(a.itemsize) # expecting 8 bytes
a / 0
8
/tmp/ipykernel_309090/1473649325.py:5: RuntimeWarning: divide by zero encountered in scalar divide
a / 0
np.float64(inf)
Προσέξτε πώς συνηθίζεται η χρήση του σύντομου ψευδώνυμου np αντί του πλήρους ονόματος της βιβλιοθήκης.
2.1.2. Σταθερές και μαθηματικές συναρτήσεις#
Η NumPy περιλαμβάνει χρήσιμες μαθηματικές συναρτήσεις και σταθερές, υπερκαλύπτοντας την εγγενή μονάδα κώδικα math. Στην συνέχεια θα χρησιμοποιήσουμε αποκλειστικά την NumPy.
import numpy as np
np.sqrt(2.0) # square root
np.exp(1.0) # exponent
np.log(10.0) # natural logarithm, inverse of exp
np.log10(10.0) # base 10 logarithm
np.sin(np.pi / 4) # sinus. Be careful, input in rad not degrees
np.tan(np.pi / 4) # tangent. Be careful, input in rad not degrees
np.float64(0.9999999999999999)
2.1.3. Αρχικοποίηση και ορισμός πινάκων#
Η αποτελεσματικότητα της NumPy οφείλεται στην υποστήριξη πολυδιάστατων πινάκων μέσω της δομής δεδομένων ndarray. Οι πίνακες ndarray αποθηκεύουν σειριακά διατεταγμένα αριθμητικά δεδομένα ίδιου τύπου. Οι διαστάσεις, το μέγεθος και ο τύπος ενός πίνακα ορίζονται κατά την δημιουργία του.
import numpy as np
N = 3 # Uppercase used to denote constants by convention (no real constants in python)
M = 2
a = np.empty(N) # uninitialized values (could be anything)
print(a)
a = np.zeros(N) # zero values
print(a)
a = np.ones(N) # values of 1
print(a)
a = np.ones((N, M)) # 2d array, N lines x M columns
print(a)
a = np.eye(N) # Identity array
print(a)
a = np.full((3, 4), 2.0) # 3 lines, 4 columns, values initialized to 2.0
print(a)
[1.14060493e-313 0.00000000e+000 0.00000000e+000]
[0. 0. 0.]
[1. 1. 1.]
[[1. 1.]
[1. 1.]
[1. 1.]]
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
[[2. 2. 2. 2.]
[2. 2. 2. 2.]
[2. 2. 2. 2.]]
# Integer vectors
a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
a = np.array([x for x in range(10)])
a = np.arange(0, 10) # from 0 to 10 (10 is excluded) with step 1
# float vectors
a = np.linspace(0, 9, 10) # from 0. to 9. in 10 equal steps
a = np.arange(0.0, 10.0)
Hint
Κατά την δημιουργία πινάκων μπορεί να χρησιμοποιηθεί το προαιρετικό όρισμα dtype για να οριστεί ο τύπος των στοιχείων. Εφόσον δεν οριστεί, η συνάρτηση δημιουργίας επιλέγει τον τύπο με βάση τα δεδομένα. Ο τύπος δεν αλλάζει κατά την χρήση του πίνακα και σε οποιαδήποτε απόδοση τιμής γίνεται πρώτα μετατροπή τύπου, εφόσον απαιτείται.
a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
a[0] = 3.14 # Conversion to int64
b = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=np.float64)
b[0] = 3.14 # b[0] is already a float64
print(type(a[0]), type(b[0]))
print(a, b)
<class 'numpy.int64'> <class 'numpy.float64'>
[3 1 2 3 4 5 6 7 8 9] [3.14 1. 2. 3. 4. 5. 6. 7. 8. 9. ]
2.1.4. Ιδιότητες πινάκων#
Η δομή ndarray εκτός από τα δεδομένα καθαυτά εμπεριέχει τις ιδιότητες του πίνακα, όπως τον τύπο των δεδομένων, τον αριθμό των διαστάσεων και το μέγεθος κάθε διάστασης. Οι ιδιότητες είναι απαραίτητες σε ορισμένες πράξεις όπως για παράδειγμα τον πολλαπλασιασμό πινάκων στις οποίες απαιτείται συμβατότητα διαστάσεων.
print(a)
print(type(a), a.dtype, a.shape, a.size, len(a))
[3 1 2 3 4 5 6 7 8 9]
<class 'numpy.ndarray'> int64 (10,) 10 10
2.1.5. Στοιχεία και τμήματα πινάκων#
Τα στοιχεία των πινάκων είναι προσβάσιμα με αγκύλες [], όπως και στις λίστες. Σε ένα πίνακα \(n\) στοιχείων, το πρώτο έχει πάντα δείκτη 0 και το τελευταίο \(n-1\).
Tip
Η διαφορετική προσέγγιση στην αρίθμηση των δεικτών είναι ένα σημείο στο οποίο πρέπει να δώσουν ιδιαίτερη προσοχή οι χρήστες του Matlab και της Fortran.
print(a[0])
print(a[9])
3
9
Με την έκφραση \([n:m:s]\) μπορεί να δηλωθεί το τμήμα ενός πίνακα (slicing).
Το \(n\) υποδηλώνει το αρχικό στοιχείο του τμήματος. Εξ’ ορισμού \(n=0\).
Το \(m\) υποδηλώνει το αμέσως επόμενο από το τελικό στοιχείο του τμήματος. Εξ’ ορισμού \(m=size\).
To \(s\) υποδηλώνει το βήμα. Εξ’ ορισμού \(s=1\).
print(a[5:]) # Elements from 6th to last (10th) with step 1
print(a[5:8]) # Elements from 6th to 8th with step 1
print(a[5:8:2]) # Elements from 6th to 8th with step 2
[5 6 7 8 9]
[5 6 7]
[5 7]
Όρια με αρνητικό πρόσημο χρησιμοποιούνται για αντίστροφη μέτρηση από το τέλος.
print(a[-3:-1])
[7 8]
Tip
Όπως και στις υπόλοιπες διατάξεις της Python, το τμήμα (slice) ενός πίνακα αποτελεί μία αναφορά στον ίδιο χώρο μνήμης και όχι ένα αντίγραφο. Για καλύτερη αξιοποίηση της μνήμης, χρησιμοποιήστε την copy σε περιπτώσεις που θέλετε να κρατήσετε ένα μικρό τμήμα ενός μεγαλύτερου πίνακα, ο οποίος δεν χρειάζεται σε κάτι άλλο. Εάν θέλετε να αντιγράψετε τιμές σε υπάρχοντα πίνακα, δηλώστε την περιοχή του πίνακα που θα δεχθεί τις νέες τιμές (π.χ. “[:]”).
b = a[0:4:2] # slice of a (sharing memory space)
c = a[0:4:2].copy() # new array, copy of a
a[0] = -1 # changes a and b but not c
print(b, c)
d = np.empty(a.size) # Create empty array
d[:] = a # copy elements to existing array, different from d=a
print(d)
[-1 2] [3 2]
[-1. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
2.1.6. Πράξεις με πίνακες#
Οι αριθμητικές πράξεις πίνακα με βαθμωτό μέγεθος (scalar) εφαρμόζονται σε όλα τα στοιχεία ομοίως. Το αποτέλεσμα είναι πίνακας όμοιου μεγέθους.
a = np.arange(0, 10)
print(a + 1)
[ 1 2 3 4 5 6 7 8 9 10]
Οι μαθηματικές συναρτήσεις την numpy με όρισμα εφαρμόζονται σε όλα τα στοιχεία ξεχωριστά και δίνουν πίνακα ομοίου μεγέθους.
print(np.sqrt(np.arange(0, 10)))
[0. 1. 1.41421356 1.73205081 2. 2.23606798
2.44948974 2.64575131 2.82842712 3. ]
Οι αριθμητικές πράξεις πίνακα με πίνακα εφαρμόζονται στοιχείο προς στοιχείο. Οι πίνακες πρέπει να έχουν το ίδιο μέγεθος.
a = np.arange(0, 10)
b = np.arange(10, 0, -1)
print(a + b)
[10 10 10 10 10 10 10 10 10 10]
Exercise 2.1
Υπολογίστε την έκφραση
Solution to Exercise 2.1
import numpy as np
A = np.array([[1, 2, 3], [4, 2, 1], [1, 1, 1]])
x = np.array([1, 4, 2])
b = A @ x # =np.dot(A,x)
print(b)
x = np.linalg.inv(A) @ b # Verification
print(x)
[15 14 7]
[1. 4. 2.]
2.1.7. Χρήσιμες συναρτήσεις#
Από τους πίνακες μπορούν εύκολα να εξαχθούν αθροίσματα και γινόμενα:
μέγιστα και ελάχιστα:
και η θέση τους στον πίνακα (η πρώτη εμφάνιση αν υπάρχουν πολλές):
import numpy as np
A = np.array([[4.0, 8.5, 9.0, 7.3, 10.5, 4.0, 5.0]])
print(np.sum(A))
print(np.prod(A))
print(np.max(A), np.argmax(A))
print(np.min(A), np.argmin(A))
48.3
469097.99999999994
10.5 4
4.0 0