Thông báo: Download 4 khóa học Python từ cơ bản đến nâng cao tại đây.
Tạo mảng cơ bản với Numpy
Trong bài này ta sẽ đào sâu và tìm hiểu kỹ về cách tạo mảng cơ bản trong NumPy, cũng như sự hiệu quả của việc dùng NumPy so với List trong việc lưu trữ và thao tác với mảng.
Python là một ngôn ngữ động (dynamic language), do vậy việc khai báo biến trên Python cũng vô cùng đơn giản, ta xét đoạn code sau:
int a = 2; if (a % 2 == 0) { printf("%d la so chan", a); }
a = 2 if a % 2 == 0: print(a, “la so chan”)
Vì C là ngôn ngữ tĩnh (static language) nên các biến phải khai báo rõ ràng. Bạn có thể thấy trước biến a cần có tiền tố int (để khai báo rằng biến a thuộc kiểu dữ liệu integer) trong khi Python thì không cần điều này.
Chẳng hạn:
Bài viết này được đăng tại [free tuts .net]
b = 5 b = “nam”
int b = 5; b = “nam” // Lỗi
Việc này khiến cho việc sử dụng Python trở nên tiện dụng hơn nhiều so với C, dù phải đánh đổi về hiệu năng. Ta không thể làm việc nhanh chóng mà phải để ý đến việc khai báo biến này thuộc kiểu dữ liệu này, kiểu dữ liệu kia, chưa kể sẽ xuất hiện hàng tá lỗi nếu set dữ liệu không cùng kiểu.
Đó cũng là lý do mà Python trở thành ngôn ngữ phổ biến nhất cho Data Science - đơn giản và tiện dụng. Trong các phần tiếp, ta sẽ tìm hiểu sâu về cơ chế hình thành nên 1 biến trong Python, đây là một khía cạnh quan trọng mà nhiều người hay bỏ qua, hiểu được những vấn đề cốt lõi này sẽ giúp việc phân tích dữ liệu một cách hiệu quả hơn.
1. Một biến trong Python được hình thành như thế nào?
Python vốn dĩ được viết trên C, do vậy hiển nhiên tất cả các biến của Python mà ta khai báo sẽ được khai báo trên C, mà ở đây chính là kiểu cấu trúc (struct).
Khi ta khai báo một số nguyên trên Python, chẳng hạn x = 100, thì nó không phải là số nguyên “thuần”, mà nó bản chất là một con trỏ và trỏ đến một struct trong C. Nếu tìm trong mã nguồn của Python 3 (CPython), một biến số nguyên được định nghĩa như thế này:
struct _longobject { long ob_refcnt; PyTypeObject *ob_type; size_t ob_size; long ob_digit[1]; };
Dễ thấy một biến số nguyên của Python gồm 4 phần:
- ob_refcnt: tham chiếu cho Python cấp phát và giải phóng bộ nhớ
- ob_type: mã hoá loại kiểu dữ liệu
- ob_size: chỉ định kích thước của các dữ liệu thành viên
- ob_digit: đây chính là nơi lưu giá trị số nguyên mà ta khai báo ban đầu
Ảnh dưới mô tả cách mà số nguyên trong C và Python được lưu trong bộ nhớ:
* Ghi chú:
PyObject_HEAD
chính là nơi chứa tất cả các tham chiếu, kiểu dữ liệu, kích thước,... đã đề cập ở trên (ob_refcnt, ob_type,...)
Từ đó, ta có thể thấy rõ được sự khác biệt của việc khai báo dữ liệu số nguyên trên Python so với C:
- Một số nguyên trong C cơ bản là một nhãn (label) cho một vị trị trong bộ nhớ mà các byte đã mã hoá giá trị số nguyên, điều này khiến cho một biến đã khai báo kiểu dữ liệu trong C thì không thể set với kiểu khác được.
- Còn trong Python, một số nguyên là một con trỏ chỉ đến một vị trí trong bộ nhớ nơi chứa một object đã để cập ở trên. Do vậy, ta có thể set kiểu dữ liệu khác mà không lo bị lỗi.
2. Cơ chế của List trong Python và sự hạn chế
Sau khi đã hiểu về cấu trúc của một biến trong Python, ta sẽ nhắc qua về List trong Python để nói về sự hạn chế của nó.
Chúng ta có thể tạo một mảng số nguyên trên Python như sau:
In [1]: A = list(range(5)) A Out [1]: [0, 1, 2, 3, 4]
In[2]: type(A[0]) Out[2]: int
Hoặc một mảng với nhiều kiểu dữ liệu:
In[3]: A1 = [True, "Freetuts", 1, 2.5] [type(i) for i in A1] Out[3]: [bool, str, int, float]
Việc khai báo một mảng với nhiều kiểu dữ liệu đem tới nhiều sự thuận lợi, tuy nhiên ta có thể thấy rõ một vấn đề sau: Nếu trong mảng đều có chung kiểu dữ liệu thì sẽ tồn tại rất nhiều thông tin thừa (tham chiếu, kiểu dữ liệu,... trong PyObject_HEAD).
Do đó, List không thực sự tốt nếu ta cần xử lý các mảng dữ liệu nếu tất cả cùng chung một kiểu dữ liệu (mà hầu hết khi xử lý dữ liệu trong Data Science, mỗi mảng sẽ chỉ có một kiểu dữ liệu duy nhất). Vì vậy, sẽ hiệu quả hơn nhiều nếu như ta cố định toàn bộ kiểu dữ liệu vào trong một mảng nếu mảng đó chung kiểu dữ liệu (fixed-type arrays). Dù phải đánh đổi sự tiện lợi nhưng nó sẽ giúp thao tác và lưu trữ hiệu quả hơn, và đó chính là cách mà NumPy làm việc.
3. Tạo mảng với Numpy
Tạo từ List
Chúng ta có thể dùng nhiều cách để tạo fixed-type arrays trong Python, chẳng hạn từ Python 3.3 đi kèm với thư viện array:
In[4]: import array A = list(range(10)) A1 = array.array('i', L) A1 Out[4]: array('i', [0, 1, 2, 3, 4])
Note: “i” chính là viết tắt cho việc mảng chứa kiểu dữ liệu integers
Đây là một thư viện khá hữu ích, tuy nhiên nó chỉ mới cung cấp khả năng lưu trữ. Với ndarray - một object cốt lõi của NumPy thì ngoài lưu trữ thì nó còn có khả năng thao tác với dữ liệu (ta sẽ nói ở các bài sau).
Có rất nhiều cách để tạo mảng với NumPy, đầu tiên ta sẽ import NumPy vào notebook:
In[5]: import numpy as np
Đầu tiên, ta có thể tạo mảng từ List bằng cách dùng np.array:
In[6]: np.array([1, 6, 9, 12]) Out[6]: array([ 1, 6, 9, 12])
Vì mảng NumPy bắt buộc phải cùng kiểu dữ liệu, nên nếu khác thì nó sẽ cố ép sao cho toàn bộ mảng cùng kiểu, chẳng hạn như:
In[7]: np.array([6.99, 1, 2, 3]) Out[7]: array([6.99, 1. , 2. , 3. ])
Ta thấy toàn bộ kiểu dữ liệu đã chuyển sang số thực để đồng bộ. Ngoài ra ta có thể khai báo trước kiểu dữ liệu của mảng:
In[8]: np.array([1, 2, 3, 4], dtype='float32') Out[8]: array([1., 2., 3., 4.], dtype=float32)
Quan trọng nhất, mảng NumPy có thể đa chiều, không giống như List chỉ có thể lưu trữ dữ liệu 1 chiều, ví dụ:
In[9]: np.array([range(i, i + 5) for i in [1, 2, 3, 4]]) Out[9]: array([[1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8]])
Tạo từ các hàm có sẵn
NumPy hỗ trợ rất nhiều hàm có sẵn để tạo mảng, tiện lợi hơn nhiều so với tạo từ List. Ta sẽ xem xét 1 số ví dụ:
In[10]: # Tạo mảng có 5 phần tử mà mọi giá trị đều bằng 0 np.zeros(5, dtype=int) Out[10]: array([0, 0, 0, 0, 0])
In[11]: # Tạo mảng đa chiều kích thước 5x5 mà mọi giá trị đều = 1 np.ones((5, 5), dtype=float) Out[11]: array([[1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.]])
In[12]: # Tạo mảng đa chiều kích thước 3x3 mà mọi giá trị đều = 100 np.full((3, 3), 100) Out[12]: array([[100, 100, 100], [100, 100, 100], [100, 100, 100]])
Ta sẽ đến với một số ví dụ nâng cao hơn, chẳng hạn:
In[13]: np.arange(0, 10, 2) Out[13]: array([0, 2, 4, 6, 8])
In[14]: np.linspace(0, 2, 5) Out[14]: array([0. , 0.5, 1. , 1.5, 2. ])
In[15]: np.random.random((3, 3)) Out[15]: array([[0.74096074, 0.52767225, 0.48543925], [0.10406574, 0.6248593 , 0.40501661], [0.52078079, 0.82908192, 0.85961909]])
In[16]: np.random.normal(0, 1, (3, 3)) Out[16]: array([[ 0.58069184, 0.00102128, 0.66747731], [ 0.92574049, -0.24678111, 0.6781257 ], [-0.60611321, -0.54344727, 0.67134354]])
In[17]: np.random.randint(0, 10, (3, 3)) Out[17]: array([[6, 2, 4], [9, 8, 8], [9, 1, 5]])
In[18]: np.eye(3) Out[18]: array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])
In[19]: np.empty(5) Out[19]: array([0. , 0.5, 1. , 1.5, 2. ])
4. Kiểu dữ liệu trong NumPy
Trong NumPy có nhiều kiểu dữ liệu khác nhau, trong bảng dưới là danh sách các kiểu dữ liệu hỗ trợ bởi NumPy (sẽ không lạ lắm với những người đã từng sử dụng C)
Các kiểu dữ liệu cơ bản của NumPy:
Kiểu dữ liệu | Chú thích |
bool_ | Kiểu Boolean, giá trị True hoặc False |
int_ | Kiểu số nguyên mặc định (giống C long; thường là int64 hoặc int32 |
intc | Giống hệt với int C (thường là int32 hoặc int64) |
intp | Số nguyên được sử dụng để lập chỉ mục (giống như C ssize_t; thông thường là int32 hoặc int64) |
int8 | Byte (–128 to 127) |
int16 | Số nguyên (–32768 đến 32767) |
int32 | Số nguyên (–2147483648 đến 2147483647) |
int64 | Số nguyên (–9223372036854775808 đến 9223372036854775807) |
uint8 | Số nguyên không dấu (0 đến 255) |
uint16 | Số nguyên không dấu (0 đến 65535) |
uint32 | Số nguyên không dấu (0 đến 4294967295) |
uint64 | Số nguyên không dấu (0 đến 18446744073709551615) |
float_ | Viết tắt cho float64 |
float16 | Half-precision float: sign bit, 5 bits exponent, 10 bits mantissa |
float32 | Single-precision float: sign bit, 8 bits exponent, 23 bits mantissa |
float64 | Double-precision float: sign bit, 11 bits exponent, 52 bits mantissa |
complex_ | Viết tắt cho complex128 |
complex64 | Số phức, được biểu diễn bởi 32 bit floats |
complex128 | Số phức, được biểu diễn bởi 64 bit floats |
* Lưu ý: Ta có thể định dạng kiểu dữ liệu của mảng bằng 2 cách:
np.zeros(10, dtype='int16')
Hoặc
np.zeros(10, dtypenp.int16)
5. Tổng kết
Qua bài trên, ta đã tìm hiểu được cơ bản về NumPy, về cách tạo mảng, kiểu dữ liệu, cũng như hiểu về cách thức mà một biến trong Python được hình thành.
Đây là một bài rất quan trọng, các bạn nên thử trên notebook tất cả các kiểu tạo mảng trên để có thể nắm bắt được các phương thức mà NumPy hỗ trợ. Trong bài tiếp theo, ta sẽ cùng nhau khám phá các thao tác xử lý mảng với NumPy. Hẹn gặp các bạn ở bài tiếp theo nhé.