concurrent.futures
¶В этом модуле описано 2 класса, с помощью которых удобно запускать параллельные вычисления.
import concurrent.futures
concurrent.futures.ThreadPoolExecutor
¶with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for i in range(100):
executor.submit(print, i, end=", ")
01, , 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1213, 14, 15, 16, 17, 18, 19, 20, 21, 22, , 23, 2425, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, , 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87888990, 91, 92, 93, , 94, 95, 96, 97, 98, 99, , ,
# Taken from Python Docs.
import urllib.request
URLS = ['http://www.foxnews.com/',
'http://www.cnn.com/',
'http://europe.wsj.com/',
'http://www.bbc.co.uk/',
'http://some-made-up-domain.com/']
# Retrieve a single page and report the URL and contents
def load_url(url, timeout):
with urllib.request.urlopen(url, timeout=timeout) as conn:
return conn.read()
# We can use a with statement to ensure threads are cleaned up promptly
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
# Start the load operations and mark each future with its URL
future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print(f"{url} generated an exception: {exc}")
else:
print(f"{url} page is {len(data)} bytes")
http://some-made-up-domain.com/ generated an exception: <urlopen error [Errno -2] Name or service not known> http://www.foxnews.com/ page is 218628 bytes http://europe.wsj.com/ generated an exception: HTTP Error 404: Not Found http://www.cnn.com/ page is 1141683 bytes http://www.bbc.co.uk/ page is 262659 bytes
concurrent.futures.ProcessPoolExecutor
¶with concurrent.futures.ProcessPoolExecutor(max_workers=10) as executor:
for i in range(100):
executor.submit(print, i, end=", ")
7, 17, 32, 42, 52, 70, 75, 84, 94, 9, 21, 31, 47, 56, 60, 63, 66, 81, 91, 0, 10, 30, 46, 57, 62, 73, 83, 97, 1, 11, 14, 23, 29, 37, 41, 51, 79, 88, 8, 20, 24, 33, 48, 58, 68, 78, 93, 98, 2, 12, 19, 34, 44, 59, 69, 72, 76, 86, 96, 4, 18, 28, 45, 55, 65, 80, 90, 5, 15, 25, 38, 50, 54, 64, 74, 95, 99, 6, 16, 26, 36, 40, 49, 53, 61, 71, 82, 87, 92, 3, 13, 22, 27, 35, 39, 43, 67, 77, 85, 89,
# Taken and modified from Python Docs.
import math
PRIMES = [
112272535095293,
112582705942171,
112272535095293,
115280095190773,
115797848077099,
1099726899285419]
def is_prime(n):
if n == 2:
return True
if n < 2 or n % 2 == 0:
return False
sqrt_n = int(math.floor(math.sqrt(n)))
for i in range(3, sqrt_n + 1, 2):
if n % i == 0:
return False
return True
with concurrent.futures.ProcessPoolExecutor() as executor:
for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
print(f"{number} is {'' if prime else 'not '}prime")
112272535095293 is prime 112582705942171 is prime 112272535095293 is prime 115280095190773 is prime 115797848077099 is prime 1099726899285419 is not prime
В чем разница между Process
и Thread
?
Попробуем понять из примера:
numbers_thread = list()
def append_thread(item):
numbers_thread.append(item)
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
for i in range(20):
executor.submit(append_thread, i)
numbers_thread
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
А что происходит с процессами?
numbers_process = list()
def append_process(item):
numbers_process.append(item)
with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
for i in range(20):
executor.submit(append_process, i)
numbers_process
[]
Каждый процесс (Process
) предоставляет все ресурсы для запуска программы.
Это значит, что каждому из них выделены свое пространство адресов, свой идентификатор, своя память и т.д. и предоставлен как минимум один поток.
Поток выполнения (Thread
) же является сущностью внутри процесса, которую тот может запускать. Потоки имеют общее пространство адресов и общие системные ресурсы (память и т.п.).
Thread
работает в рамках вашей сессии и может менять переменные внутри нее.
Process
работает в отдельной сессии: для любого изменения вашей переменной необходимо ее скопировать в другую сессию.
Как вы думаете, что из этого работает быстрее?
Давайте проверим:
Создадим функцию, которая будет работать примерно секунду.
def long_sum():
s = 0
for i in range(20_000_000 + 1):
s += i
return s
%%time
long_sum()
CPU times: user 1.06 s, sys: 1.23 ms, total: 1.06 s Wall time: 1.06 s
200000010000000
%%time
for _ in range(10):
long_sum()
CPU times: user 10.1 s, sys: 0 ns, total: 10.1 s Wall time: 10.1 s
Теперь попробуем посчитать эту сумму несколько раз параллельно:
%%time
with concurrent.futures.ProcessPoolExecutor(max_workers=10) as executor:
for _ in range(10):
executor.submit(long_sum)
CPU times: user 18 ms, sys: 26.3 ms, total: 44.3 ms Wall time: 2.97 s
%%time
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for _ in range(10):
executor.submit(long_sum)
CPU times: user 14.2 s, sys: 217 ms, total: 14.4 s Wall time: 14.1 s
Какая-то ерунда; почему в случае Thread
расчет идет еще дольше, чем в простом цикле?
Ответ заключается в архитектуре CPython.
Идея состоит в том, что в Python реализован механизм Global Interpreter Lock (GIL), запрещающий отдельным потокам внутри процесса Python выполняться одновременно.
Как выполняются три потока одновременно. Источник - Wikipedia Commons.
Зачем тогда вообще нужны потоки?
Потоки медленнее только при активных вычислениях; быстрее потоки могут:
a) Ждать:
import time
%%time
for _ in range(10):
time.sleep(2)
CPU times: user 2.26 ms, sys: 33 µs, total: 2.29 ms Wall time: 20 s
%%time
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for _ in range(10):
executor.submit(time.sleep, 2)
CPU times: user 2.26 ms, sys: 3.93 ms, total: 6.19 ms Wall time: 2 s
Зачем вообще может понадобиться ждать?
Когда вы запускаете множество подпроцессов, вам приходится ждать их завершения (как десятки картирований на геном, сотни докингов и т.п.). Потоки прекрасно справятся с такой задачей.
b) Работать с операциями, на которые не распространяется GIL.
Обычно это потенциально блокирующие или очень длительные операции, как ввод-вывод (на экран и в файлы), обработка изображений или NumPy "number crunching".
a = numpy.arange(1_000_000)
a
array([ 0, 1, 2, ..., 999997, 999998, 999999])
%%time
sum(a)
CPU times: user 194 ms, sys: 0 ns, total: 194 ms Wall time: 193 ms
499999500000
%%time
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for _ in range(100):
executor.submit(sum, a)
CPU times: user 19.9 s, sys: 136 ms, total: 20.1 s Wall time: 19.8 s
%%time
numpy.sum(a)
CPU times: user 2.32 ms, sys: 25 µs, total: 2.35 ms Wall time: 1.24 ms
499999500000
%%time
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for _ in range(100):
executor.submit(numpy.sum, a)
CPU times: user 119 ms, sys: 4.17 ms, total: 123 ms Wall time: 23.8 ms
c) Когда требуется одновременная работа двух разных алгоритмов.
Наиболее простой пример - responsive user interface.
Ваша программа не должна полностью зависать во время подсчета; именно это и реализуется потоками.
Простое правило:
multiprocessing
;threading
;threading
;threading
.NumPy [/ˈnʌmpaɪ/ (NUM-py)] - библиотека языка Python, добавляющая поддержку многомерных массивов и матриц, а также целую коллекцию формул и высокоуровневых математических функций для работы с массивами и матрицами.
import numpy as np
np.__version__
'1.17.2'
Давайте подумаем, зачем нужен целый модуль для математики, массивов и матриц?
Мне будет достаточно списков! - they said.
numpy
?¶numpy
?¶Сколько программистов нужно, чтобы заменить лампочку?
Ни одного - проблема тут по части железа.
Numpy нужен для векторизованных вычислений!
mln = 10 ** 6
a = list(range(mln))
%%timeit
[e * e for e in a]
61.7 ms ± 548 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
a = np.arange(mln)
%%timeit
a * a
2.31 ms ± 48.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Какая операция здесь самая медленная?
Конечно же, цикл!
Ещё дольше выполняются вложенные циклы:
rows, cols = 1000, 1000
a = [list(range(i, i + cols)) for i in range(0, rows)]
b = [[0 for j in range(cols)] for i in range(rows)]
%%timeit
for i in range(rows):
for j in range(cols):
b[i][j] = 2 * a[i][j]
133 ms ± 2.05 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Сравним с векторизованными циклами:
a = np.asarray([list(range(i, i + cols)) for i in range(0, rows)])
%%timeit
2 * a
2.42 ms ± 85.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Выигрыш во времени примерно в 50 раз!
numpy.array
¶Почему в массиве важно иметь один и тот же тип данных?
Самый простой способ создать массив numpy.array
- из стандартного списка list
.
mydata = np.array([1, 2, 3])
mydata
array([1, 2, 3])
Массив может быть и многомерным!
mydata_2d = np.array([[1, 2],
[3, 4]])
mydata_2d
array([[1, 2], [3, 4]])
mydata = np.array([[1, 2, 3, 4, 5],
[6, 7, 8, 9, 0],
[7, 0, 8, 8, 1]], dtype=float)
mydata
array([[1., 2., 3., 4., 5.], [6., 7., 8., 9., 0.], [7., 0., 8., 8., 1.]])
mydata.dtype
dtype('float64')
numpy
¶Тип данных | Описание |
---|---|
bool | Boolean (True or False) stored as a byte |
int | Platform integer (normally either int32 or int64) |
int8 | Byte (-128 to 127) |
int16 | Integer (-32768 to 32767) |
int32 | Integer (-2147483648 to 2147483647) |
int64 | Integer (-9223372036854775808 to 9223372036854775807) |
uint8 | Unsigned integer (0 to 255) |
uint16 | Unsigned integer (0 to 65535) |
uint32 | Unsigned integer (0 to 4294967295) |
uint64 | Unsigned integer (0 to 18446744073709551615) |
float | Shorthand for float64 |
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 | Shorthand for complex128 |
complex64 | Complex number, represented by two 32-bit floats (real and imaginary components) |
complex128 | Complex number, represented by two 64-bit floats (real and imaginary components) |
mydata.astype(np.int8)
array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 0], [7, 0, 8, 8, 1]], dtype=int8)
mydata = np.array([[1, 2, 3, 4, 5],
[6, 7, 8, 9, 0],
[7, 0, 8, 8, 1]], dtype=np.int8)
mydata
array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 0], [7, 0, 8, 8, 1]], dtype=int8)
len(mydata)
3
mydata.ndim
2
mydata.shape
(3, 5)
mydata.strides
(5, 1)
numpy.array.shape
¶data = np.array([1, 2, 3, 4, 5, 6], dtype=np.int8)
data.reshape(2, 3)
array([[1, 2, 3], [4, 5, 6]], dtype=int8)
data.reshape(3, 2)
array([[1, 2], [3, 4], [5, 6]], dtype=int8)
data
array([1, 2, 3, 4, 5, 6], dtype=int8)
data.reshape(2, -1)
array([[1, 2, 3], [4, 5, 6]], dtype=int8)
data.reshape(-1, 3)
array([[1, 2, 3], [4, 5, 6]], dtype=int8)
data.reshape(3, 3)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-46-013b97b673e4> in <module> ----> 1 data.reshape(3, 3) ValueError: cannot reshape array of size 6 into shape (3,3)
data = np.array([[1, 2, 3],
[4, 5, 6]], dtype=np.int8)
data2 = data.reshape(-1)
data2
array([1, 2, 3, 4, 5, 6], dtype=int8)
data[0,2] = -1
data2
array([ 1, 2, -1, 4, 5, 6], dtype=int8)
Что это означает? :^)
data = np.array([[1, 2, 3],
[4, 5, 6]], dtype=np.int8)
a = data.ravel()
a
array([1, 2, 3, 4, 5, 6], dtype=int8)
b = data.flatten()
b
array([1, 2, 3, 4, 5, 6], dtype=int8)
data[0,0] = -1
a, b
(array([-1, 2, 3, 4, 5, 6], dtype=int8), array([1, 2, 3, 4, 5, 6], dtype=int8))
Что значит "транспонировать матрицу"?
Как транспонировать в стандартном Python?
list_data = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
list_T = list(zip(*list_data))
list_T
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
data = np.array(list_data)
data.T
array([[1, 4, 7], [2, 5, 8], [3, 6, 9]])
data.transpose()
array([[1, 4, 7], [2, 5, 8], [3, 6, 9]])
numpy.array
...¶Но сначала... Кто знает, что называют индусским кодом? :^)
int a0 = 0;
int a1 = 0;
int a2 = 0;
int a3 = 0;
int a4 = 0;
int a5 = 0;
int a6 = 0;
int a7 = 0;
int a8 = 0;
int a9 = 0;
Допустим, мы хотим сделать массив numpy.array
из нулей.
Сделаем способом, который помним!
x = 10
y = 3
zeros = []
for i in range(y):
sub_zero = []
for j in range(x):
sub_zero.append(0)
zeros.append(sub_zero)
zeros = np.array(zeros)
zeros
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
В стандартном Python:
x = 10
y = 3
zeros = [[0 for _ in range(x)] for _ in range(y)]
zeros
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
[[0] * x] * y
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
np.zeros(shape=(3, 10), dtype=int)
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
Чтобы создавать простые массивы numpy.array
, можно воспользоваться специальными функциями:
np.ones(3)
array([1., 1., 1.])
np.ones(shape=(3,))
array([1., 1., 1.])
np.random.random(3)
array([0.57664778, 0.72303613, 0.44506472])
np.full(3, fill_value=-8.0)
array([-8., -8., -8.])
Аналогично с многомерными массивами:
np.zeros(shape=(3, 2))
array([[0., 0.], [0., 0.], [0., 0.]])
np.random.random((2, 3))
array([[0.42878116, 0.01896822, 0.23738247], [0.68345334, 0.5032586 , 0.86208521]])
np.full((2, 4), fill_value="F")
array([['F', 'F', 'F', 'F'], ['F', 'F', 'F', 'F']], dtype='<U1')
Массив такого же размера, но из нулей:
data
array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
np.zeros_like(data)
array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
E - единичный массив
np.eye(5)
array([[1., 0., 0., 0., 0.], [0., 1., 0., 0., 0.], [0., 0., 1., 0., 0.], [0., 0., 0., 1., 0.], [0., 0., 0., 0., 1.]])
Аналоги range
, сразу возвращающие массив numpy.array
:
np.arange(1, 10)
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
np.arange(1, 10, 2)
array([1, 3, 5, 7, 9])
np.arange(1, 10, 0.5)
array([1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. , 7.5, 8. , 8.5, 9. , 9.5])
np.linspace(0, 1, 5, endpoint=True)
array([0. , 0.25, 0.5 , 0.75, 1. ])
В чем разница?
— Какие храбрые поступки вы совершали в своей жизни?
— Однажды на алгебре я руку поднял.
data = np.array([[1, 2],
[3, 4],
[5, 6]], dtype=np.int8)
data + 2
array([[3, 4], [5, 6], [7, 8]], dtype=int8)
data - 5
array([[-4, -3], [-2, -1], [ 0, 1]], dtype=int8)
1 - data
array([[ 0, -1], [-2, -3], [-4, -5]], dtype=int8)
data * 2
array([[ 2, 4], [ 6, 8], [10, 12]], dtype=int8)
data / 3
array([[0.33333333, 0.66666667], [1. , 1.33333333], [1.66666667, 2. ]])
data ** 2
array([[ 1, 4], [ 9, 16], [25, 36]], dtype=int8)
2 ** data
array([[ 2, 4], [ 8, 16], [32, 64]], dtype=int8)
np.add(data, 2)
array([[3, 4], [5, 6], [7, 8]], dtype=int8)
np.subtract(data, 5)
array([[-4, -3], [-2, -1], [ 0, 1]], dtype=int8)
np.subtract(1, data)
array([[ 0, -1], [-2, -3], [-4, -5]], dtype=int8)
np.multiply(data, 2)
array([[ 2, 4], [ 6, 8], [10, 12]], dtype=int8)
np.divide(data, 3)
array([[0.33333333, 0.66666667], [1. , 1.33333333], [1.66666667, 2. ]])
np.power(data, 2)
array([[ 1, 4], [ 9, 16], [25, 36]], dtype=int8)
np.power(2, data)
array([[ 2, 4], [ 8, 16], [32, 64]], dtype=int8)
Как вы думаете, что произойдет, если попытаться сложить 2 массива?
data = np.array([1, 2])
ones = np.ones(2)
data + ones
array([2., 3.])
С другими операциями это тоже работает:
data - ones
array([0., 1.])
data * data
array([1, 4])
data / data
array([1., 1.])
А что получится с многомерными массивами?
data = np.array([[1, 2],
[3, 4]], dtype=int)
ones = np.ones(shape=(2, 2))
Всё так же!
data + ones
array([[2., 3.], [4., 5.]])
Можно складывать и разные по размерностям массивы.
data = np.array([[1, 2],
[3, 4],
[5, 6]])
ones_row = np.ones(2)
data + ones_row
array([[2., 3.], [4., 5.], [6., 7.]])
data.shape
(3, 2)
data + np.ones(2)
array([[2., 3.], [4., 5.], [6., 7.]])
data + np.ones(3)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-103-59adb053f41f> in <module> ----> 1 data + np.ones(3) ValueError: operands could not be broadcast together with shapes (3,2) (3,)
data + np.ones(shape=(3, 1))
array([[2., 3.], [4., 5.], [6., 7.]])
Почему для 2D-массивов 1D-массив прибавляется по axis=1
, но не прибавляется по axis=0
?
Еще раз; как идут измерения:
Как понять, можно ли произвести бинарную операцию (типа сложения) для пары массивов?
Посмотрим на пример:
data = np.arange(30).reshape(3, -1)
data
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]])
adder = np.arange(data.shape[1])
adder
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print(f"data.shape = {data.shape}")
print(f"adder.shape = {adder.shape}")
data.shape = (3, 10) adder.shape = (10,)
data + adder
array([[ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18], [10, 12, 14, 16, 18, 20, 22, 24, 26, 28], [20, 22, 24, 26, 28, 30, 32, 34, 36, 38]])
data
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]])
adder = np.arange(data.shape[0])
adder
array([0, 1, 2])
print(f"data.shape = {data.shape}")
print(f"adder.shape = {adder.shape}")
data.shape = (3, 10) adder.shape = (3,)
data + adder
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-112-e23945759bf2> in <module> ----> 1 data + adder ValueError: operands could not be broadcast together with shapes (3,10) (3,)
data
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]])
adder = np.arange(data.shape[0]).reshape(-1, 1)
adder
array([[0], [1], [2]])
print(f"data.shape = {data.shape}")
print(f"adder.shape = {adder.shape}")
data.shape = (3, 10) adder.shape = (3, 1)
data + adder
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [11, 12, 13, 14, 15, 16, 17, 18, 19, 20], [22, 23, 24, 25, 26, 27, 28, 29, 30, 31]])
На самом деле существуют специальное правило приведения размерностей:
a.shape = (a_1, a_2, ..., a_n)
и b.shape = (b_1, b_2, ..., b_n)
. Над a
и b
можно произвести поэлементую бинарную операцию, если $\forall \; i \in (1...n)$ выполнено хотя бы одно из условий:a_i == b_i
;a_i == 1
;b_i == 1
.Документация: https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html
(из материалов Техносферы@Mail.Ru)
Проверить явное приведение размерности можно через функции np.broadcast_to
и np.broadcast_arrays
.
Такие же правила применяются к вычитанию.
data = np.array([1, 3])
ones = np.ones(2)
data - ones
array([0., 2.])
data = np.array([[1, 2],
[3, 4]])
ones = np.ones(shape=(2, 2))
data - ones
array([[0., 1.], [2., 3.]])
А как будет происходить умножение?
data1 = np.array([[1, 2],
[3, 4]])
data2 = np.array([[0, 10],
[20, -1]])
data1 * data2
array([[ 0, 20], [60, -4]])
Но что делать, если нам нужно матричное умножение?
data = np.array([1, 2, 3])
powers_of_ten = (10 ** np.arange(6)).reshape(3, 2)
powers_of_ten
array([[ 1, 10], [ 100, 1000], [ 10000, 100000]])
data.dot(powers_of_ten)
array([ 30201, 302010])
Для тех, кто забыл, что такое матричное умножение:
А теперь перейдем от базовой арифметики к чуть более cложным функциям.
Подождите, это все была базовая арифметика?
data = np.array([[1, 2, 3],
[4, 5, 8]])
np.sqrt(data)
array([[1. , 1.41421356, 1.73205081], [2. , 2.23606798, 2.82842712]])
np.exp(data)
array([[2.71828183e+00, 7.38905610e+00, 2.00855369e+01], [5.45981500e+01, 1.48413159e+02, 2.98095799e+03]])
np.log(data)
array([[0. , 0.69314718, 1.09861229], [1.38629436, 1.60943791, 2.07944154]])
np.log2(data)
array([[0. , 1. , 1.5849625 ], [2. , 2.32192809, 3. ]])
np.random.seed(777)
a = np.random.randint(0, 6, size=(8,))
a[3] = 10
a
array([ 3, 1, 5, 10, 1, 2, 0, 2])
a.min(), a.max(), a.argmax(), a.sum(), a.prod(), a.mean()
(0, 10, 3, 24, 0, 3.0)
np.min(a), np.max(a), np.argmax(a), np.sum(a), np.prod(a), np.mean(a)
(0, 10, 3, 24, 0, 3.0)
Старайтесь не использовать с массивами numpy.array
нативные функции min
, max
и sum
.
Какая у этого причина?
a = np.random.randint(-2, 8, size=(6,))
max(a), a
(6, array([ 3, 6, -1, 5, 5, 0]))
a = np.random.randint(-2, 8, size=(4, 3))
max(a), a
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-133-5c91eed52270> in <module> 1 a = np.random.randint(-2, 8, size=(4, 3)) ----> 2 max(a), a ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
Почему агрегирующие операции?
Просто потому что они выполняют агрегацию (редукцию) по определенной оси!
Разберем, что делает numpy.array.???(axis=axis)
– агрегирующая операция вдоль оси axis
:
axis
;axis
из исходного массива.Что все это значит? Как это применять?
Разберем пример:
data = np.array([[1, 2],
[5, 3],
[4, 6]], dtype=int)
data.max(axis=0)
array([5, 6])
data.max(axis=1)
array([2, 5, 6])
np.random.seed(777)
data = np.random.randint(0, 10, size=(3, 7))
data[1, 3] = 15
data
array([[ 7, 6, 7, 1, 7, 4, 7], [ 9, 8, 7, 15, 0, 1, 2], [ 4, 5, 7, 1, 7, 2, 2]])
data.argmax()
10
data.ravel()[data.argmax()]
15
i = np.argmax(data)
i, j = i // data.shape[1], i % data.shape[1]
print(f"value = {data[i, j]}")
print(f"index = {(i, j)}")
value = 15 index = (1, 3)
np.unravel_index(np.argmax(data), data.shape)
(1, 3)
data = np.arange(6)
data
array([0, 1, 2, 3, 4, 5])
data = data > 2
data
array([False, False, False, True, True, True])
data.any(), np.any(data), any(data)
(True, True, True)
data.all(), np.all(data), all(data)
(False, False, False)
~data
array([ True, True, True, False, False, False])
data = np.arange(12).reshape(3, 4) >= 3
data
array([[False, False, False, True], [ True, True, True, True], [ True, True, True, True]])
data.all()
False
data.all(axis=0), data.all(axis=1)
(array([False, False, False, True]), array([False, True, True]))
all(data)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-150-4eb86e975eae> in <module> ----> 1 all(data) ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
a = np.array([1, 1, 0, 0, 1], dtype=bool)
b = np.array([1, 0, 0, 1, 0], dtype=bool)
a and b
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-152-61df3bd186ad> in <module> ----> 1 a and b ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
a & b
array([ True, False, False, False, False])
a | b
array([ True, True, False, True, True])
a ^ b
array([False, True, False, True, True])
numpy.array
¶Общее правило: массив[первый индекс:последний индекс:шаг]
.
Значения по умолчанию:
data = np.array([1, 2, 3], dtype=np.int8)
print(f"data[0] = {data[0]}")
print(f"data[1] = {data[1]}")
print(f"data[0:2] = {data[0:2]}")
print(f"data[1:] = {data[1:]}")
data[0] = 1 data[1] = 2 data[0:2] = [1 2] data[1:] = [2 3]
Для многомерных массивов индексация немного отличается от default Python.
Вспомним, как индексируются обычные вложенные списки?
data = [[ 10, 12],
[ 28, "Oh, fish!"]]
data[1][1]
'Oh, fish!'
data = np.array(data)
data[1, 1]
'Oh, fish!'
data = np.array([[1, 2],
[3, 4],
[5, 6]])
data[0,1], data[1,0]
(2, 3)
data[1:3]
array([[3, 4], [5, 6]])
data[0:2,0]
array([1, 3])
data = np.arange(10)
ind = np.array([1, 0, 0, 1, 0, 1, 1, 1, 1, 0], dtype=bool)
data, ind
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), array([ True, False, False, True, False, True, True, True, True, False]))
data[ind]
array([0, 3, 5, 6, 7, 8])
Возможно, это одна из самых крутых "фишек" NumPy!
Почему? Чем это полезно?
Можно очень легко делать фильтры! Например:
np.random.seed(777)
a = np.random.randint(-2, 7, 34)
a
array([ 5, 4, 5, -1, 5, 2, 5, 6, 5, 0, -2, -1, 0, 2, 3, 5, -1, 5, 0, 0, 5, 2, 6, 4, 0, 5, 6, -2, 6, 1, 0, -2, 1, 1])
Пусть мы хотим найти все отрицательные элементы.
a < 0
array([False, False, False, True, False, False, False, False, False, False, True, True, False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, True, False, False, False, True, False, False])
a[a < 0]
array([-1, -2, -1, -1, -2, -2])
Найдем все элементы, кратные 3 (сравнимые с нулем по модулю 3):
a
array([ 5, 4, 5, -1, 5, 2, 5, 6, 5, 0, -2, -1, 0, 2, 3, 5, -1, 5, 0, 0, 5, 2, 6, 4, 0, 5, 6, -2, 6, 1, 0, -2, 1, 1])
a[a % 3 == 0]
array([6, 0, 0, 3, 0, 0, 6, 0, 6, 6, 0])
a[np.logical_not(a % 3)]
array([6, 0, 0, 3, 0, 0, 6, 0, 6, 6, 0])
a[~((a % 3).astype(bool))]
array([6, 0, 0, 3, 0, 0, 6, 0, 6, 6, 0])
Найдем все элементы, кратные 3 или 5:
a
array([ 5, 4, 5, -1, 5, 2, 5, 6, 5, 0, -2, -1, 0, 2, 3, 5, -1, 5, 0, 0, 5, 2, 6, 4, 0, 5, 6, -2, 6, 1, 0, -2, 1, 1])
del35 = (a % 3 == 0) | (a % 5 == 0)
del35
array([ True, False, True, False, True, False, True, True, True, True, False, False, True, False, True, True, False, True, True, True, True, False, True, False, True, True, True, False, True, False, True, False, False, False])
a[del35]
array([5, 5, 5, 5, 6, 5, 0, 0, 3, 5, 5, 0, 0, 5, 6, 0, 5, 6, 6, 0])
Функция np.where
позволит преобразовать булев массив в индексы.
np.random.seed(777)
a = np.random.randint(-10, 10, size=(3, 5))
a
array([[ -3, 5, -4, 7, -3], [ -3, 4, -3, 8, 3], [ 4, -10, -9, 8, -5]])
bool_array = a > 0
bool_array
array([[False, True, False, True, False], [False, True, False, True, True], [ True, False, False, True, False]])
where = np.where(bool_array)
where
(array([0, 0, 1, 1, 1, 2, 2]), array([1, 3, 1, 3, 4, 0, 3]))
np.random.seed(777)
np.random.rand(10)
array([0.15266373, 0.30235661, 0.06203641, 0.45986034, 0.83525338, 0.92699705, 0.72698898, 0.76849622, 0.26920507, 0.64402929])
np.random.randint(0, 10, 10)
array([1, 2, 4, 5, 7, 1, 7, 2, 2, 7])
np.random.permutation(10)
array([1, 5, 0, 3, 9, 7, 2, 6, 8, 4])
np.random.choice(10, size=10)
array([8, 3, 2, 0, 3, 3, 4, 0, 6, 5])
np.random.seed(777)
a = np.random.randint(1, 10, 12).reshape(4, 3)
a
array([[8, 7, 8], [2, 8, 5], [8, 9, 8], [3, 1, 2]])
b = np.random.randint(-5, 0, 9).reshape(3, 3)
b
array([[-3, -1, -4], [-2, -3, -3], [-3, -1, -5]])
np.concatenate((a, b))
array([[ 8, 7, 8], [ 2, 8, 5], [ 8, 9, 8], [ 3, 1, 2], [-3, -1, -4], [-2, -3, -3], [-3, -1, -5]])
А как склеить по другой оси?
a.T
array([[8, 2, 8, 3], [7, 8, 9, 1], [8, 5, 8, 2]])
b
array([[-3, -1, -4], [-2, -3, -3], [-3, -1, -5]])
np.concatenate((a.T, b), axis=1)
array([[ 8, 2, 8, 3, -3, -1, -4], [ 7, 8, 9, 1, -2, -3, -3], [ 8, 5, 8, 2, -3, -1, -5]])
np.hstack((a.T, b)) # concatenate with axis=1
array([[ 8, 2, 8, 3, -3, -1, -4], [ 7, 8, 9, 1, -2, -3, -3], [ 8, 5, 8, 2, -3, -1, -5]])
np.vstack((a, b)) # concatenate with axis=0
array([[ 8, 7, 8], [ 2, 8, 5], [ 8, 9, 8], [ 3, 1, 2], [-3, -1, -4], [-2, -3, -3], [-3, -1, -5]])
np.random.seed(777)
data = np.random.randint(1, 20, 10)
data
array([ 8, 16, 7, 18, 8, 8, 15, 8, 19, 14])
np.sort(data), data
(array([ 7, 8, 8, 8, 8, 14, 15, 16, 18, 19]), array([ 8, 16, 7, 18, 8, 8, 15, 8, 19, 14]))
data.sort(), data
(None, array([ 7, 8, 8, 8, 8, 14, 15, 16, 18, 19]))
Сортировка индексов
data = np.random.randint(1, 20, 10)
data
array([15, 1, 2, 19, 6, 8, 2, 8, 11, 19])
np.argsort(data)
array([1, 2, 6, 4, 5, 7, 8, 0, 3, 9])
data[np.argsort(data)]
array([ 1, 2, 2, 6, 8, 8, 11, 15, 19, 19])
Подождите, в NumPy можно индексировать по массиву индексов?
Да, это называется "Fancy indexing".
np.random.seed(1337)
a = np.random.randint(-5, 5, size=(5, 5))
a
array([[ 2, 3, 2, 4, 2], [-3, -3, -1, 3, 4], [ 1, 1, 2, 3, -4], [ 1, 1, -3, -3, 4], [ 3, -4, 2, -2, -4]])
Как получить 2, 4 и 3 строки?
a[[2, 4, 3]]
array([[ 1, 1, 2, 3, -4], [ 3, -4, 2, -2, -4], [ 1, 1, -3, -3, 4]])
a
array([[ 2, 3, 2, 4, 2], [-3, -3, -1, 3, 4], [ 1, 1, 2, 3, -4], [ 1, 1, -3, -3, 4], [ 3, -4, 2, -2, -4]])
Как получить элементы в строках 2, 4, 3 и в 0 и последнем столбце?
a[[2, 4, 3], [0, -1]]
--------------------------------------------------------------------------- IndexError Traceback (most recent call last) <ipython-input-202-9ed845fbe954> in <module> ----> 1 a[[2, 4, 3], [0, -1]] IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (3,) (2,)
a[[2, 4, 3], [0, -1, 2]]
array([ 1, -4, -3])
a[[2, 4, 3]][:,[0, -1]]
array([[ 1, -4], [ 3, -4], [ 1, 4]])
Fancy indexing может применяться и так:
a
array([[ 2, 3, 2, 4, 2], [-3, -3, -1, 3, 4], [ 1, 1, 2, 3, -4], [ 1, 1, -3, -3, 4], [ 3, -4, 2, -2, -4]])
where = np.where(a >= 0)
where
(array([0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4]), array([0, 1, 2, 3, 4, 3, 4, 0, 1, 2, 3, 0, 1, 4, 0, 2]))
a[where]
array([2, 3, 2, 4, 2, 3, 4, 1, 1, 2, 3, 1, 1, 4, 3, 2])
Можно сделать еще и так:
a
array([[ 2, 3, 2, 4, 2], [-3, -3, -1, 3, 4], [ 1, 1, 2, 3, -4], [ 1, 1, -3, -3, 4], [ 3, -4, 2, -2, -4]])
took = a.take([2, 4, 3], axis=1)
took
array([[ 2, 2, 4], [-1, 4, 3], [ 2, -4, 3], [-3, 4, -3], [ 2, -4, -2]])
a[0,4] = 100
took
array([[ 2, 2, 4], [-1, 4, 3], [ 2, -4, 3], [-3, 4, -3], [ 2, -4, -2]])
Последняя тема:
view
от copy
¶Что есть что?
view
- разные объекты ссылаются на одно и то же пространство в памяти.
copy
- пространство в памяти от "старого" объекта было скопировано, "новый" объект ссылается на копию.
Как отличить одно от другого?
data = np.random.randint(-5, 5, size=(2, 5))
data
array([[-2, 4, -2, -1, 3], [ 3, 2, 4, -1, -4]])
v = data[:,1:4]
v
array([[ 4, -2, -1], [ 2, 4, -1]])
v.base is data
True
v
- это view
*¶*не vendetta.
data
array([[-2, 4, -2, -1, 3], [ 3, 2, 4, -1, -4]])
c = data[:,[1, 2, 3]]
c
array([[ 4, -2, -1], [ 2, 4, -1]])
c.base is data
False
c
- это copy
.¶view
и copy
?¶view
изменяется и исходный массив.copy
занимает обычно в ~2 раза больше времени (чем больше массив, тем хуже).view
имеет ту же базу (numpy.array.base
), что и исходный массив; copy
имеет в качестве базы другой объект.view
расположен в том же участке памяти, что и исходный массив, copy
же занимает дополнительную память.Соответственно, если вам нужен и исходный массив и его измененная версия, то вам нужно использовать copy
.
Если же ваш массив занимает очень много памяти или если вам нужно создать очень много срезов массива, то следует использовать view
.
В большинстве остальных случаев нет существенной разницы.
view
, а когда - copy
?¶View | Copy | |
---|---|---|
Slices | Indexing, e.g. a[0,:] |
Fancy indexing, e.g. a[[0],:] |
Changing dtype |
- | a.as_type(np.float32) |
Converting to 1D array | a.ravel() |
a.flatten() |
Вы всегда получите копию, если скопируете объект :^)
unambiguous_copy = data.copy()
unambiguous_copy.base is data
False
Полную документацию NumPy смотрите на сайте:
https://docs.scipy.org/doc/numpy
В NumPy есть еще множество потенциально полезных для вас функций, так что не поленитесь зайти и посмотреть :^)