Форум программистов, компьютерный форум, киберфорум
py-thonny
Войти
Регистрация
Восстановить пароль

Python NumPy: Лучшие практики и примеры

Запись от py-thonny размещена 17.03.2025 в 11:56
Показов 1854 Комментарии 0

Нажмите на изображение для увеличения
Название: 2238ea62-4867-4ef8-965b-16ecb8ed443c.jpg
Просмотров: 62
Размер:	149.0 Кб
ID:	10435
NumPy (Numerical Python) — одна из ключевых библиотек для научных вычислений в Python. Она превращает Python из просто удобного языка общего назначения в среду для проведения сложных математических операций и обработки массивов данных. Если вы работаете с большими объёмами числовых данных, то NumPy должен быть в вашем арсенале инструментов. Ядро библиотеки — это многомерный массив ndarray. В отличие от стандартных списков Python, ndarray хранит элементы одного типа и предоставляет операции над ними, оптимизированные для производительности. Это фундаментальное различие и объясняет, почему NumPy многократно превосходит стандартные структуры данных Python в скорости вычислений.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np
 
# Создание массива из списка
arr1 = np.array([1, 2, 3, 4, 5])
print(arr1)  # [1 2 3 4 5]
 
# Создание двумерного массива
arr2 = np.array([[1, 2, 3], [4, 5, 6]])
print(arr2)  # [[1 2 3]
             #  [4 5 6]]
 
# Получение информации о массиве
print(f"Форма: {arr2.shape}")     # Форма: (2, 3)
print(f"Размерность: {arr2.ndim}")  # Размерность: 2
print(f"Тип данных: {arr2.dtype}")  # Тип данных: int64
NumPy предлагает множество функций для создания массивов. Одна из самых полезных — np.arange(), которая генерирует массив чисел в заданном диапазоне:

Python
1
2
3
4
5
6
7
# Создать массив 0-9
arr = np.arange(10)
print(arr)  # [0 1 2 3 4 5 6 7 8 9]
 
# С указанным шагом
arr = np.arange(0, 10, 2)
print(arr)  # [0 2 4 6 8]
Для создания массива с равномерно распределёнными точками можно использовать np.linspace():

Python
1
2
3
# 5 чисел между 0 и 1
arr = np.linspace(0, 1, 5)
print(arr)  # [0.   0.25 0.5  0.75 1.  ]
Одной из уникальных особенностей NumPy является возможность создания массивов с заданным типом данных (dtype). Контроль над типами данных критически важен для оптимизации памяти и производительности:

Python
1
2
3
4
5
6
7
8
# Создание массива целых чисел
int_arr = np.array([1, 2, 3], dtype=np.int32)
 
# Создание массива чисел с плавающей точкой
float_arr = np.array([1, 2, 3], dtype=np.float64)
 
# Преобразование типа
converted = int_arr.astype(np.float32)
NumPy также позволяет создавать массивы особых форм:

Python
1
2
3
4
5
6
7
8
9
# Матрица единиц 3x3
ones = np.ones((3, 3))
print(ones)
 
# Матрица нулей 2x4
zeros = np.zeros((2, 4))
 
# Единичная матрица 4x4
identity = np.eye(4)
Операции с массивами в NumPy векторизованы, что означает их выполнение над всеми элементами сразу. Это не только делает код более читаемым, но и значительно ускоряет вычисления:

Python
1
2
3
4
5
6
7
8
9
10
11
# Арифметические операции
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
 
print(a + b)  # [5 7 9]
print(a * b)  # [4 10 18]
print(a ** 2)  # [1 4 9]
 
# Тригонометрические и другие функции
angles = np.array([0, np.pi/2, np.pi])
print(np.sin(angles))  # [0.0, 1.0, 0.0]
Многие программисты задаются вопросом, когда стоит использовать NumPy вместо стандартных структур данных Python. Вот основное правило: если у вас есть числовые данные и вы проводите математические операции над ними, особенно если они большие или многомерные, NumPy почти всегда будет лучшим выбором. Для демонстрации разницы в производительности проведём простой тест — сложение элементов в массиве размером 1 миллион:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time
 
# Python список
py_list = list(range(1000000))
start = time.time()
sum_list = sum(py_list)
py_time = time.time() - start
 
# NumPy массив
np_array = np.arange(1000000)
start = time.time()
sum_array = np.sum(np_array)
np_time = time.time() - start
 
print(f"Python: {py_time:.6f} с")
print(f"NumPy:  {np_time:.6f} с")
print(f"NumPy в {py_time/np_time:.1f} раз быстрее")

Установка NumPy



Установка NumPy проста, достаточно выполнить:

Python
1
pip install numpy
Или, если вы используете Anaconda:

Python
1
conda install numpy

Побольше практики в Python
Всем привет, занимаюсь изучением Python, но теорию изучать это одно, хотелось бы побольше практики:). Посоветуйте задачники по Python, помимо проекта...

По поводу практики Python
Пока читал книгу введения в язык(a byte of python) не загонялся по поводу практики,хотя и до этого знал и видел что пишут на счет этого,нужно...

Лучшие книги для начинающего программиста на python
Какие лучшие книги по python для начинающих?

Python, numpy, матрицы
Подскажите пожалуйста, есть ли способ найти побочную диагональ и параллельные ей диагонали в квадратичной матрице с помощью библиотеки numpy?


Интеграция с Pandas, SciPy и Matplotlib



NumPy прекрасно интегрируется с другими библиотеками экосистемы научного Python. Pandas, SciPy и Matplotlib построены на основе NumPy и работают с его массивами напрямую. Это создаёт целостную экосистему для анализа данных:

Python
1
2
3
4
5
6
7
8
# Пример использования NumPy с Pandas
import pandas as pd
import numpy as np
 
# Создание DataFrame из массива NumPy
np_array = np.random.rand(5, 3)
df = pd.DataFrame(np_array, columns=['A', 'B', 'C'])
print(df)
Интеграция с SciPy расширяет возможности NumPy специализированными функциями для оптимизации, интегрирования, обработки сигналов и других научных задач:

Python
1
2
3
4
5
6
7
8
9
from scipy import optimize
import numpy as np
 
# Минимизация функции с использованием NumPy и SciPy
def f(x):
    return x**2 + 10*np.sin(x)
 
result = optimize.minimize(f, x0=0)
print(f"Минимум найден в точке x = {result.x[0]:.4f}")
Визуализация данных становится простой с помощью Matplotlib, который тоже работает напрямую с массивами NumPy:

Python
1
2
3
4
5
6
7
8
9
10
11
12
import matplotlib.pyplot as plt
 
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
 
plt.figure(figsize=(8, 4))
plt.plot(x, y)
plt.title('Синусоида')
plt.xlabel('x')
plt.ylabel('sin(x)')
plt.grid(True)
plt.show()

Типы данных NumPy



При работе с NumPy важно понимать особенности системы типов данных. В отличие от Python, где тип переменной может меняться, в NumPy типы фиксированы при создании массива. Это обеспечивает эффективность операций, но требует дополнительного внимания. Базовые типы данных в NumPy:
np.int8, np.int16, np.int32, np.int64: целые числа разной разрядности.
np.uint8, np.uint16, np.uint32, np.uint64: беззнаковые целые.
np.float16, np.float32, np.float64: числа с плавающей точкой.
np.complex64, np.complex128: комплексные числа.
np.bool_: логические значения.

Преобразование между типами (кастинг) — частая операция при работе с NumPy:

Python
1
2
3
4
5
6
7
8
# Явное преобразование
float_array = np.array([1.1, 2.2, 3.3])
int_array = float_array.astype(np.int32)
print(int_array)  # [1 2 3]
 
# Кастинг при создании
mixed_array = np.array([1, 2.5, 3])  # Результат: float
print(mixed_array.dtype)  # float64
При выполнении операций между массивами разных типов NumPy автоматически приводит результат к наиболее вместительному типу:

Python
1
2
3
4
int8_array = np.array([1, 2, 3], dtype=np.int8)
float32_array = np.array([0.1, 0.2, 0.3], dtype=np.float32)
result = int8_array + float32_array
print(result.dtype)  # float32
Эта система приведения типов (type promotion) следует определённым правилам, описанным в документации NumPy.
Стоит отметить, что неправильный выбор типа данных может привести к проблемам переполнения и потери точности:

Python
1
2
3
4
5
6
7
# Переполнение int8
x = np.array([127], dtype=np.int8)
print(x + 1)  # [-128] (переполнение)
 
# Потеря точности при преобразовании float в int
y = np.array([1.9], dtype=np.float32)
print(y.astype(np.int32))  # [1] (отбрасывается дробная часть)
NumPy также позволяет определять пользовательские типы данных с помощью np.dtype, что особенно полезно при работе со структурированными данными:

Python
1
2
3
4
5
# Создание структурированного типа данных
dt = np.dtype([('name', 'U10'), ('age', np.int32), ('weight', np.float32)])
person = np.array([('Анна', 25, 58.5), ('Иван', 30, 75.0)], dtype=dt)
print(person['name'])  # ['Анна' 'Иван']
print(person[0])  # ('Анна', 25, 58.5)
Когда речь заходит о сравнении производительности NumPy с чистым Python, мне вспоминается один забавный случай. Когда-то я потратил два дня на оптимизацию алгоритма обработки изображений с помощью всевозможных трюков Python, только чтобы обнаружить, что простая реализация на NumPy работает в 200 раз быстрее. Это хорошая иллюстрация того, почему важно выбирать правильные инструменты для задачи. Однако стоит понимать, что NumPy — не панацея. Для некоторых задач стандартные структуры данных Python могут быть лучшим выбором:
1. Когда данные разнородны (разные типы в одной коллекции).
2. Когда требуется частое добавление или удаление элементов.
3. Для небольших наборов данных, где накладные расходы на создание массива NumPy не окупаются.
4. Когда скорость не критична.

NumPy — фундаментальная библиотека, знание которой открывает двери к экосистеме научного Python. Освоив базовые принципы работы с массивами NumPy, вы получите прочную основу для изучения более специализированных библиотек, таких как pandas, sklearn или TensorFlow.

Продвинутое индексирование



Одной из самых мощных возможностей NumPy является его продвинутая система индексирования, которая выходит далеко за рамки простого обращения к элементам по их позициям. Умело используя эти техники, можно значительно упростить код и повысить его производительность.

Булево и фантомное индексирование



Начнем с булевого индексирования – техники, которая позволяет выбирать элементы массива на основе их значений. Это особенно полезно для фильтрации данных:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
 
# Создаем массив
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
 
# Булево индексирование: выбираем только чётные числа
mask = arr % 2 == 0
evens = arr[mask]
print(evens)  # [0 2 4 6 8]
 
# Краткая запись для фильтрации
greater_than_five = arr[arr > 5]
print(greater_than_five)  # [6 7 8 9]
Булево индексирование превосходно работает и с многомерными массивами:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Двумерный массив
matrix = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])
 
# Фильтрация строк по условию
row_filter = np.array([True, False, True])  # Выбираем первую и третью строки
print(matrix[row_filter])
# [[1 2 3]
[H2] [7 8 9]][/H2]
 
# Фильтрация элементов по значению
print(matrix[matrix > 5])  # [6 7 8 9]
Обратите внимание, что во втором примере результатом является одномерный массив – при фильтрации по значениям структура матрицы не сохраняется. Особый интерес представляет фантомное (fancy) индексирование, которое позволяет использовать массивы индексов для извлечения данных:

Python
1
2
3
4
5
6
7
8
9
10
# Оригинальный массив
arr = np.arange(10)
 
# Извлекаем элементы с индексами 1, 3, 5, 7
indices = np.array([1, 3, 5, 7])
selected = arr[indices]
print(selected)  # [1 3 5 7]
 
# Можно использовать списки напрямую
print(arr[[0, 2, 4]])  # [0 2 4]
Для многомерных массивов фантомное индексирование становится еще интереснее:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
# Создаем матрицу 5x5
matrix = np.arange(25).reshape(5, 5)
print(matrix)
# [[ 0  1  2  3  4]
#  [ 5  6  7  8  9]
#  [10 11 12 13 14]
#  [15 16 17 18 19]
[H2] [20 21 22 23 24]][/H2]
 
# Выбираем элементы по парам координат (строка, столбец)
rows = np.array([0, 1, 3])
cols = np.array([1, 2, 0])
print(matrix[rows, cols])  # [1 7 15]
Здесь происходит что-то интересное: мы выбираем элементы с координатами (0,1), (1,2) и (3,0), то есть элементы 1, 7 и 15 из матрицы.

Техника "умных срезов" для многомерных массивов



А что, если нам нужно выбрать несколько строк и столбцов целиком? Для этого используется комбинация срезов и фантомного индексирования:

Python
1
2
3
4
5
6
7
# Выбираем строки 0 и 2, столбцы 0, 1 и 4
selected_rows = np.array([0, 2])
selected_cols = np.array([0, 1, 4])
submatrix = matrix[selected_rows[:, np.newaxis], selected_cols]
print(submatrix)
# [[ 0  1  4]
#  [10 11 14]]
Здесь np.newaxis (или эквивалентно None) используется для добавления нового измерения, что позволяет правильно сформировать результирующую матрицу. Техника "умных срезов" позволяет выбирать сложные подмножества многомерных массивов. Рассмотрим несколько трюков:

Python
1
2
3
4
5
6
7
8
9
10
# Трюк 1: Выборка элементов диагонали матрицы
diag = np.arange(9).reshape(3, 3)
print(diag[np.arange(3), np.arange(3)])  # [0 4 8]
 
# Трюк 2: Обращение осей
reversed_cols = matrix[:, ::-1]  # Переворачиваем столбцы
reversed_rows = matrix[::-1, :]  # Переворачиваем строки
 
# Трюк 3: Выборка каждого n-го элемента
every_second = arr[::2]  # [0 2 4 6 8]

Маскирование и фильтрация



Одно из самых мощных применений продвинутого индексирования – маскирование данных. Например, мы можем заменить все отрицательные значения в массиве нулями:

Python
1
2
3
4
5
6
7
8
9
10
11
# Массив с отрицательными значениями
data = np.array([1, -2, 3, -4, 5])
 
# Создаем копию и маскируем отрицательные значения
masked = data.copy()
masked[masked < 0] = 0
print(masked)  # [1 0 3 0 5]
 
# Или можно использовать np.where для условной замены
result = np.where(data < 0, 0, data)
print(result)  # [1 0 3 0 5]
Функция np.where особенно удобна – она работает как тернарный оператор, выбирая элементы из двух массивов в зависимости от условия. Еще одна интересная техника – индексирование вдоль осей. Допустим, нам нужно найти максимальное значение в каждой строке матрицы:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Матрица 3x4
mat = np.array([
    [5, 9, 2, 7],
    [1, 6, 3, 8],
    [4, 0, 5, 2]
])
 
# Находим индексы максимальных элементов по строкам
max_indices = np.argmax(mat, axis=1)
print(max_indices)  # [1 3 2]
 
# Теперь получаем сами максимальные значения
rows = np.arange(len(mat))
max_values = mat[rows, max_indices]
print(max_values)  # [9 8 5]
Продвинутое индексирование открывает широкие возможности в обработке изображений. Например, можно легко реализовать пороговую сегментацию:

Python
1
2
3
4
5
6
7
8
9
10
# Предположим, image - это изображение в виде массива NumPy
image = np.random.randint(0, 256, (100, 100))  # Имитируем изображение
 
# Пороговая сегментация
binary = np.zeros_like(image)
binary[image > 128] = 1  # Все пиксели ярче 128 становятся белыми
 
# Или можно применить обработку только к определенной области изображения
roi = image[20:50, 30:70]  # Region of Interest - область интереса
roi[roi < 50] = 0  # Подавляем шум в выделенной области
Иногда бывает полезно комбинировать несколько условий. Для этого используются логические операторы (&, |, ~):

Python
1
2
3
4
5
6
7
8
9
10
11
# Создаем массив значений
values = np.random.randint(-10, 10, 20)
 
# Выбираем только положительные четные числа
mask = (values > 0) & (values % 2 == 0)
result = values[mask]
print(result)
 
# Выбираем числа, которые либо меньше -5, либо больше 5
extreme_values = values[(values < -5) | (values > 5)]
print(extreme_values)
Важно помнить, что при использовании логических операторов с массивами NumPy нужно заключать каждое условие в скобки, иначе могут возникнуть неожиданные результаты из-за приоритета операций. Продвинутое индексирование не только делает код более компактным, но и зачастую значительно ускоряет его по сравнению с эквивалентными циклами Python. Это связано с тем, что операции выполняются на низком уровне, оптимизированном C-кодом, а не интерпретатором Python. Помимо рассмотренных уже техник, продвинутое индексирование позволяет также эффективно работать с многомерными массивами и извлекать из них ценную информацию несколькими способами.

Часто возникает задача извлечения элементов по диагонали матрицы. NumPy предоставляет для этого специальную функцию:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
matrix = np.arange(9).reshape(3, 3)
print(matrix)
# [[0 1 2]
#  [3 4 5]
[H2] [6 7 8]][/H2]
 
# Получаем главную диагональ
diagonal = np.diag(matrix)
print(diagonal)  # [0 4 8]
 
# Можно получить и другие диагонали, используя смещение
upper_diag = np.diag(matrix, k=1)
print(upper_diag)  # [1 5]
 
lower_diag = np.diag(matrix, k=-1)
print(lower_diag)  # [3 7]
Функцию np.diag можно использовать и для создания диагональных матриц:

Python
1
2
3
4
5
6
7
# Создаем диагональную матрицу из вектора
diag_matrix = np.diag([1, 2, 3, 4])
print(diag_matrix)
# [[1 0 0 0]
#  [0 2 0 0]
#  [0 0 3 0]
#  [0 0 0 4]]
Очень полезной техникой является использование индексирования для модификации элементов массива по маске. Например, нормализация данных:

Python
1
2
3
4
5
6
7
8
# Данные с выбросами
data = np.array([2, 3, 5, 700, 1, 2, 4, 800])
 
# Находим выбросы (значения > 100) и заменяем их медианой
median = np.median(data)
outliers = data > 100
data[outliers] = median
print(data)  # [2. 3. 5. 3. 1. 2. 4. 3.]
При обработке изображений часто требуется обработка определённых участков. Маскирование позволяет делать это элегантно:

Python
1
2
3
4
5
6
7
8
9
10
11
# Предположим, у нас есть изображение в градациях серого
image = np.random.randint(0, 256, (10, 10))
 
# Создаем маску в форме круга
y, x = np.ogrid[:10, :10]
center = (4, 4)
radius = 3
mask = (x - center[0])[B]2 + (y - center[1])[/B]2 <= radius**2
 
# Применяем операцию только к пикселям внутри круга
image[mask] = 255  # Делаем круг белым
Для более сложных форм масок может быть полезна библиотека SciPy:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from scipy import ndimage
 
# Создаем структурный элемент в форме креста
struct = ndimage.generate_binary_structure(2, 1)
print(struct)
# [[False  True False]
#  [ True  True  True]
[H2] [False  True False]][/H2]
 
# Используем его как маску
small_image = np.zeros((5, 5))
center = (2, 2)
small_image[center[0]-1:center[0]+2, center[1]-1:center[1]+2][struct] = 1
print(small_image)
# [[0. 0. 0. 0. 0.]
#  [0. 0. 1. 0. 0.]
#  [0. 1. 1. 1. 0.]
#  [0. 0. 1. 0. 0.]
#  [0. 0. 0. 0. 0.]]
Еще один мощный инструмент — индексирование с помощью кортежей. Он позволяет работать с отдельными элементами многомерного массива:

Python
1
2
3
4
5
6
7
# Создаем 3D массив
cube = np.arange(27).reshape(3, 3, 3)
 
# Выбираем элементы с координатами (0,0,0), (1,1,1) и (2,2,2)
coords = ([0, 1, 2], [0, 1, 2], [0, 1, 2])
values = cube[coords]
print(values)  # [ 0 13 26]
А что если нам нужно заменить определенные элементы массива? Фантомное индексирование отлично подходит и для этой задачи:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
# Исходный массив
arr = np.arange(10)
print(arr)  # [0 1 2 3 4 5 6 7 8 9]
 
# Мы хотим заменить элементы с индексами 0, 3 и 6 на 99
indices = [0, 3, 6]
arr[indices] = 99
print(arr)  # [99  1  2 99  4  5 99  7  8  9]
 
# Можно даже заменить на разные значения
other_indices = [1, 4, 7]
arr[other_indices] = [100, 101, 102]
print(arr)  # [ 99 100   2  99 101   5  99 102   8   9]
Особенно полезным становится индексирование при работе с временными рядами или данными экспериментов. Допустим, у нас есть временной ряд, и мы хотим выбрать значения только в определенные моменты времени:

Python
1
2
3
4
5
6
7
8
9
10
11
12
# Имитация временного ряда за 100 дней
days = np.arange(100)
values = np.sin(days * 0.1) + np.random.normal(0, 0.1, 100)
 
# Выбираем только значения по понедельникам (каждый 7-й день)
mondays = days[days % 7 == 0]
monday_values = values[mondays]
 
# Или выбираем периоды высокой активности
high_activity = values > 0.5
high_periods = days[high_activity]
print(f"Дни высокой активности: {high_periods}")
Иногда требуется найти ближайшие значения в массиве к заданному набору точек. Например, при интерполяции или подгонке данных:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
# Отсортированный массив
sorted_arr = np.sort(np.random.rand(1000))
 
# Точки, для которых ищем ближайшие значения
query_points = np.array([0.2, 0.5, 0.7])
 
# Находим индексы ближайших значений
indices = np.searchsorted(sorted_arr, query_points)
print(indices)
 
# Получаем сами ближайшие значения
nearest = sorted_arr[indices]
print(nearest)
Функция np.searchsorted выполняет бинарный поиск, что очень эффективно для больших отсортированных массивов.
Очень полезным приемом является использование булевого индексирования для реализации условной логики внутри вычислений:

Python
1
2
3
4
5
6
7
8
# Массив с некоторыми отсутствующими значениями (NaN)
data = np.array([1.0, 2.0, np.nan, 4.0, np.nan])
 
# Заменяем NaN на среднее от доступных значений
valid = ~np.isnan(data)
mean_value = np.mean(data[valid])
result = np.where(valid, data, mean_value)
print(result)  # [1.  2.  2.33333333 4.  2.33333333]
Часто встречается задача группировки данных по определенному признаку. Рассмотрим, как с помощью продвинутого индексирования можно сделать такую группировку:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
# Допустим, у нас есть данные о температуре по месяцам
months = np.array([1, 1, 2, 2, 3, 3, 1, 2, 3])  # Месяцы
temps = np.array([5, 7, 10, 12, 15, 17, 6, 11, 16])  # Температуры
 
# Вычисляем среднюю температуру для каждого месяца
unique_months = np.unique(months)
average_temps = np.zeros_like(unique_months, dtype=float)
 
for i, month in enumerate(unique_months):
    mask = months == month
    average_temps[i] = np.mean(temps[mask])
 
print(f"Средние температуры по месяцам: {average_temps}")
Более эффективный способ решить эту задачу — использовать функцию np.bincount с весами:

Python
1
2
3
4
5
6
7
# Тот же пример, но с использованием bincount
indices = months - 1  # Приводим к 0-индексации
n_months = np.max(months)
counts = np.bincount(indices, minlength=n_months)
total_temps = np.bincount(indices, weights=temps, minlength=n_months)
average_temps = total_temps / counts
print(f"Средние температуры по месяцам: {average_temps}")
Индексирование может быть задействовано и для реализации скользящего окна при анализе временных рядов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Создаем временной ряд
ts = np.arange(20)
 
# Функция для создания скользящих окон заданной ширины
def rolling_window(a, window):
    shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
    strides = a.strides + (a.strides[-1],)
    return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
 
# Создаем окна шириной 3
windows = rolling_window(ts, 3)
print(windows)
# [[ 0  1  2]
#  [ 1  2  3]
#  [ 2  3  4]
#  ...
[H2] [17 18 19]][/H2]
 
# Теперь можно применять операции к каждому окну
rolling_mean = np.mean(windows, axis=1)
print(rolling_mean)  # [ 1.  2.  3. ... 18.]
Этот прием с использованием stride tricks — малоизвестная, но крайне мощная техника в арсенале NumPy, которая позволяет создавать представления массивов с перекрывающимися элементами без дополнительных затрат памяти. Она широко применяется в обработке сигналов, изображений и временных рядов.

Векторизация вычислений



Одним из главных преимуществ NumPy является возможность векторизации вычислений. Векторизация — это процесс переписывания кода таким образом, чтобы избежать явных циклов, заменяя их операциями над целыми массивами. Это не просто делает код более читаемым, но и значительно повышает его производительность. Суть векторизации заключается в том, что операции выполняются параллельно над всеми элементами массива, вместо последовательной обработки каждого элемента в цикле. Такой подход позволяет задействовать низкоуровневые оптимизации и, в некоторых случаях, использовать возможности параллельных вычислений на аппаратном уровне. Рассмотрим простой пример — вычисление евклидова расстояния между двумя точками в многомерном пространстве:

Python
1
2
3
4
5
6
7
8
9
10
# Неоптимальный подход с циклом
def euclidean_distance_loop(p1, p2):
    sum_squared = 0
    for i in range(len(p1)):
        sum_squared += (p1[i] - p2[i]) ** 2
    return np.sqrt(sum_squared)
 
# Векторизованный подход
def euclidean_distance_vec(p1, p2):
    return np.sqrt(np.sum((p1 - p2) ** 2))
Давайте измерим, насколько быстрее работает векторизованная версия:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np
import time
 
# Генерируем точки в 1000-мерном пространстве
p1 = np.random.randn(1000)
p2 = np.random.randn(1000)
 
# Измеряем время для версии с циклом
start = time.time()
for _ in range(1000):
    d1 = euclidean_distance_loop(p1, p2)
loop_time = time.time() - start
 
# Измеряем время для векторизованной версии
start = time.time()
for _ in range(1000):
    d2 = euclidean_distance_vec(p1, p2)
vec_time = time.time() - start
 
print(f"Время с циклом: {loop_time:.6f} сек")
print(f"Время векторизованной версии: {vec_time:.6f} сек")
print(f"Ускорение: {loop_time/vec_time:.1f}x")
На моей машине векторизованная версия работает примерно в 50-100 раз быстрее. Это впечатляющее ускорение достигается благодаря тому, что NumPy делегирует выполнение операций оптимизированным библиотекам на C/C++, которые могут использовать SIMD-инструкции процессора (Single Instruction, Multiple Data).

Теперь рассмотрим более сложный пример — вычисление матрицы расстояний между множеством точек:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Генерируем 1000 точек в 3D пространстве
points = np.random.randn(1000, 3)
 
# Неоптимальный подход с вложенными циклами
def distance_matrix_loop(points):
    n = len(points)
    result = np.zeros((n, n))
    for i in range(n):
        for j in range(n):
            result[i, j] = np.sqrt(np.sum((points[i] - points[j]) ** 2))
    return result
 
# Векторизованный подход 
def distance_matrix_vec(points):
    # Используем трюк с broadcasting
    diff = points[:, np.newaxis, :] - points[np.newaxis, :, :]
    return np.sqrt(np.sum(diff ** 2, axis=-1))
Измерим производительность на небольшом подмножестве:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
small_points = points[:100]  # Берём только 100 точек для теста
 
# Цикл
start = time.time()
dm1 = distance_matrix_loop(small_points)
loop_time = time.time() - start
 
# Векторизация
start = time.time()
dm2 = distance_matrix_vec(small_points)
vec_time = time.time() - start
 
print(f"Время с циклом: {loop_time:.6f} сек")
print(f"Время векторизованной версии: {vec_time:.6f} сек")
print(f"Ускорение: {loop_time/vec_time:.1f}x")
В этом случае ускорение может достигать сотен раз! Но что делает векторизованную версию такой быстрой? Здесь используется прием, известный как "broadcasting" — способность NumPy работать с массивами разных форм, автоматически расширяя их до совместимых размерностей. В нашем примере выражение points[:, np.newaxis, :] - points[np.newaxis, :, :] создает разности между всеми парами точек одновременно, без необходимости явных циклов.

Еще один пример — вычисление скользящего среднего для временного ряда:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Генерируем временной ряд
ts = np.random.randn(10000)
 
# Подход с циклом
def moving_average_loop(array, window):
    result = np.zeros_like(array)
    for i in range(len(array)):
        if i < window - 1:
            # Для начальных элементов берем среднее по доступным значениям
            result[i] = np.mean(array[:i+1])
        else:
            result[i] = np.mean(array[i-window+1:i+1])
    return result
 
# Векторизованный подход с использованием свертки
def moving_average_vec(array, window):
    weights = np.ones(window) / window
    return np.convolve(array, weights, mode='same')
Измерим скорость для окна шириной 50:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window_size = 50
 
# Цикл
start = time.time()
ma1 = moving_average_loop(ts, window_size)
loop_time = time.time() - start
 
# Векторизация
start = time.time()
ma2 = moving_average_vec(ts, window_size)
vec_time = time.time() - start
 
print(f"Время с циклом: {loop_time:.6f} сек")
print(f"Время векторизованной версии: {vec_time:.6f} сек")
print(f"Ускорение: {loop_time/vec_time:.1f}x")
Здесь мы использовали функцию np.convolve для выполнения свертки — операции, которая эффективно вычисляет скользящее среднее. Это пример того, как высокоуровневые функции NumPy могут заменять сложные циклические вычисления.

Векторизация не только ускоряет код, но и позволяет использовать многоядерные процессоры. NumPy может автоматически распараллеливать некоторые операции с помощью библиотеки BLAS (Basic Linear Algebra Subprograms) и других оптимизированных библиотек. Вот пример, где мы явно включаем многопоточность при вычислении матричного произведения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Создаем большие матрицы
A = np.random.rand(1000, 1000)
B = np.random.rand(1000, 1000)
 
# Последовательное вычисление
np.dot(A, B)
 
# Параллельное вычисление (если доступно)
np.show_config()  # Проверка конфигурации NumPy
 
# Если NumPy собран с поддержкой многопоточности:
import os
os.environ['OMP_NUM_THREADS'] = '4'  # Используем 4 потока
result = np.dot(A, B)  # Теперь выполняется параллельно
Это позволяет добиться еще большего ускорения на многоядерных системах.

Но векторизация имеет и свои ограничения. Не все алгоритмы можно эффективно векторизовать. Например, алгоритмы с зависимостью по данным, где каждый шаг зависит от результата предыдущего, сложно представить в векторной форме. Рассмотрим пример — вычисление чисел Фибоначчи:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Традиционный подход с циклом
def fibonacci_loop(n):
    if n <= 1:
        return n
    fib = np.zeros(n+1, dtype=np.uint64)
    fib[1] = 1
    for i in range(2, n+1):
        fib[i] = fib[i-1] + fib[i-2]
    return fib[n]
 
# Попытка векторизации (работает только для нескольких элементов!)
def fibonacci_matrix_power(n):
    F = np.array([[1, 1], [1, 0]], dtype=np.uint64)
    if n == 0:
        return 0
    power = np.linalg.matrix_power(F, n-1)
    return power[0, 0]
Здесь традиционный подход с циклом более понятен, хотя матричное возведение в степень тоже работает благодаря математическому свойству чисел Фибоначчи. Но для очень больших n даже метод с матрицами становится неэффективным из-за ограничений на размер целых чисел. Другой пример — поиск простых чисел с помощью Решета Эратосфена:

Python
1
2
3
4
5
6
7
8
9
10
# Подход с циклом
def primes_loop(n):
    sieve = np.ones(n+1, dtype=bool)
    sieve[0:2] = False
    for i in range(2, int(np.sqrt(n))+1):
        if sieve[i]:
            sieve[i*i::i] = False
    return np.where(sieve)[0]
 
# Здесь элемент векторизации уже присутствует: sieve[i*i::i] = False
В этом алгоритме уже используется форма векторизации при установке флагов для кратных чисел. Это хороший пример того, как даже в алгоритмах с циклами можно применять векторные операции для ускорения критических участков. Иногда полная векторизация невозможна, но можно векторизовать части алгоритма:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Частично векторизованный алгоритм k-means
def kmeans_partial_vec(X, k, max_iter=100):
    # Инициализация центроидов
    centroids = X[np.random.choice(len(X), k, replace=False)]
    
    for _ in range(max_iter):
        # Векторизованное вычисление расстояний до всех центроидов
        distances = np.sqrt(((X[:, np.newaxis, :] - centroids[np.newaxis, :, :]) ** 2).sum(axis=2))
        
        # Найти ближайший центроид для каждой точки
        labels = np.argmin(distances, axis=1)
        
        # Обновить центроиды (тоже векторизованно)
        new_centroids = np.array([X[labels == i].mean(axis=0) for i in range(k)])
        
        # Проверка сходимости
        if np.all(centroids == new_centroids):
            break
            
        centroids = new_centroids
    
    return labels, centroids
В этом примере мы векторизовали вычисление расстояний между точками и центроидами, что является самой вычислительно затратной частью алгоритма k-means.

Эффективное управление памятью



Понимание того, как NumPy управляет памятью, критически важно для создания эффективных программ, особенно при работе с большими наборами данных. В отличие от стандартных структур данных Python, массивы NumPy имеют особое внутреннее устройство, которое позволяет им обеспечивать высокую производительность, но также требует и особого подхода.

Начнём с базового понимания устройства массивов NumPy. Каждый массив состоит из двух основных компонентов:
1. Блок памяти, содержащий сами данные.
2. Метаданные, описывающие форму, тип и шаг массива.

Python
1
2
3
4
5
6
7
8
9
10
11
import numpy as np
 
# Создаём простой массив
arr = np.array([1, 2, 3, 4, 5], dtype=np.int32)
 
# Смотрим метаданные
print(f"Форма: {arr.shape}")          # (5,)
print(f"Тип данных: {arr.dtype}")      # int32
print(f"Размер элемента: {arr.itemsize} байт")   # 4 байта
print(f"Общий размер: {arr.nbytes} байт")       # 20 байт
print(f"Шаг: {arr.strides}")          # (4,) байта
Параметр strides (шаг) — особенно интересен. Он указывает, сколько байт нужно "перепрыгнуть", чтобы перейти к следующему элементу по каждой оси. В одномерном массиве это просто размер одного элемента, но в многомерных массивах понимание шагов становится очень важным.

Одной из особенностей NumPy является то, что при выполнении многих операций не создаются новые массивы, а возвращаются представления (views) исходного массива. Это позволяет экономить память и время на копирование данных. Однако эта особенность может привести к неожиданным результатам, если её не учитывать:

Python
1
2
3
4
5
6
7
8
9
10
11
12
# Создаём массив
original = np.array([1, 2, 3, 4, 5])
print(f"Оригинал: {original}")  # [1 2 3 4 5]
 
# Создаём срез — это представление, а не копия
view = original[1:4]
print(f"Срез: {view}")  # [2 3 4]
 
# Изменим элемент в срезе
view[0] = 99
print(f"Изменённый срез: {view}")        # [99 3 4]
print(f"Оригинал после изменения: {original}")  # [1 99 3 4 5]
Как видите, изменение элемента в представлении повлияло на исходный массив. Это происходит потому, что представление и оригинал используют один и тот же блок памяти. Когда нужно создать независимую копию массива, следует явно использовать метод .copy():

Python
1
2
3
4
5
# Создаём копию
copy = original.copy()
copy[0] = 100
print(f"Копия после изменения: {copy}")       # [100 99 3 4 5]
print(f"Оригинал после изменения копии: {original}")  # [1 99 3 4 5]
Как определить, является ли массив представлением другого массива? Для этого можно использовать атрибут base:

Python
1
2
3
print(f"base оригинала: {original.base}")  # None - не является представлением
print(f"base среза: {view.base is original}")  # True - является представлением original
print(f"base копии: {copy.base}")  # None - не является представлением
Понимание разницы между копированием и представлением особенно важно при работе с большими массивами, где лишнее копирование может привести к заметным накладным расходам по памяти и времени.
Еще одним интересным аспектом является порядок хранения данных в памяти. NumPy поддерживает два порядка: C (строки за строками) и Fortran (столбцы за столбцами):

Python
1
2
3
4
5
6
7
# Создаём матрицу в порядке C (по умолчанию)
c_order = np.array([[1, 2, 3], [4, 5, 6]], order='C')
print(f"Шаги C-массива: {c_order.strides}")  # (12, 4) байта
 
# Создаём матрицу в порядке Fortran
f_order = np.array([[1, 2, 3], [4, 5, 6]], order='F')
print(f"Шаги F-массива: {f_order.strides}")  # (4, 8) байта
Порядок хранения влияет на эффективность доступа к элементам массива. В общем случае, доступ к элементам вдоль последней оси быстрее в C-порядке, а вдоль первой оси — в Fortran-порядке.
При работе с большими массивами данных иногда возникают проблемы с утечкой памяти. Это может происходить, когда представления больших массивов остаются в памяти дольше, чем нужно. Например:

Python
1
2
3
4
5
6
7
8
def process_chunk(data):
sub = data[:, 10:20]  # Представление части большого массива
# ... обработка sub ...
return result
 
# Для большого массива
large_data = np.random.rand(10000, 10000)
results = [process_chunk(large_data) for _ in range(100)]
В этом примере, даже если нам нужен только небольшой кусочек large_data, полный массив будет оставаться в памяти, пока существует хотя бы одно его представление. Чтобы избежать этой проблемы, можно создавать копии нужных частей:

Python
1
2
3
4
def process_chunk(data):
sub = data[:, 10:20].copy()  # Независимая копия
# ... обработка sub ...
return result
Однако создание копий тоже требует дополнительной памяти и времени, поэтому в каждом конкретном случае нужно выбирать оптимальный подход.

При работе с памятью в NumPy важно понимать концепцию "broadcasting" (трансляции). Она позволяет выполнять операции над массивами разных форм, автоматически расширяя их до совместимого размера. Это очень удобно, но может привести к неожиданному расходу памяти:

Python
1
2
3
4
5
6
7
# Маленькие массивы
a = np.ones((10, 1))
b = np.ones((1, 10000))
 
# Операция с трансляцией - будет создан временный массив размера (10, 10000)
c = a * b
print(f"Размер результата: {c.nbytes} байт")  # Довольно большой!
Для больших массивов такой подход может быстро исчерпать доступную память. В таких случаях лучше использовать пошаговый подход или специальные функции NumPy, оптимизированные для работы с большими данными.

Еще одной стратегией оптимизации памяти является выбор правильного типа данных. NumPy поддерживает множество типов, от однобайтовых целых чисел до 128-битных комплексных чисел:

Python
1
2
3
4
5
6
7
# Массив с 64-битными числами с плавающей точкой (по умолчанию)
default = np.ones(1000000)
print(f"Размер с float64: {default.nbytes} байт")  # 8000000 байт
 
# Тот же массив, но с 32-битными числами
optimized = np.ones(1000000, dtype=np.float32)
print(f"Размер с float32: {optimized.nbytes} байт")  # 4000000 байт
Если вам известен диапазон значений в массиве, вы можете выбрать тип с минимально необходимым размером. Например, если вы знаете, что все значения положительные и меньше 256, можно использовать np.uint8, который занимает всего 1 байт на элемент.

Для очень больших наборов данных, которые не помещаются в память, NumPy предлагает различные решения. Одним из них является np.memmap, который позволяет работать с файлами на диске как с массивами в памяти:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Создаём файл на диске размером 1 ГБ
big_array = np.memmap('big_array.dat', dtype=np.float32, mode='w+', 
                     shape=(250000000,))  # ~1 ГБ при float32
 
# Работаем с ним как с обычным массивом
big_array[0] = 42
big_array[1:1000] = np.random.rand(999)
 
# Изменения сохраняются на диск при удалении объекта или явном вызове flush
big_array.flush()
 
# Позже можно снова открыть файл
reopened = np.memmap('big_array.dat', dtype=np.float32, mode='r+', 
                    shape=(250000000,))
print(reopened[0])  # 42.0
Это позволяет работать с массивами, размер которых превышает доступную оперативную память.

Еще одной техникой является использование разреженных массивов (sparse arrays) для данных, в которых большинство элементов равны нулю:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
from scipy import sparse
 
# Создаём разреженную матрицу (99% нулей)
size = 10000
data = np.random.rand(size, size)
mask = np.random.rand(size, size) > 0.99  # Только 1% ненулевых элементов
data[~mask] = 0
 
# Преобразуем в разреженный формат
sparse_matrix = sparse.csr_matrix(data)
 
print(f"Размер полной матрицы: {data.nbytes / 1e6:.1f} МБ")
print(f"Размер разреженной матрицы: {sparse_matrix.data.nbytes / 1e6:.1f} МБ")
В этом примере разреженное представление может быть в 100 раз меньше по размеру, чем полная матрица.

При работе с временными массивами, которые создаются во время вычислений, можно использовать технику "in-place" операций для экономии памяти:

Python
1
2
3
4
5
6
7
8
9
# Создаём большие массивы
a = np.random.rand(1000000)
b = np.random.rand(1000000)
 
# Стандартная операция создаёт новый массив
c = a + b
 
# In-place операция модифицирует существующий массив
np.add(a, b, out=a)  # Результат сохраняется прямо в a
Многие функции NumPy поддерживают параметр out, позволяющий указать, куда записывать результат. Это помогает избежать создания лишних временных массивов. Если у вас возникают проблемы с памятью, полезно знать, как отследить использование памяти массивами NumPy. Можно использовать функцию из модуля sys:

Python
1
2
3
4
5
6
import sys
 
# Измеряем размер объекта в памяти
a = np.zeros(1000000, dtype=np.float64)
print(f"Размер массива: {a.nbytes / 1e6:.1f} МБ")
print(f"Общий размер объекта: {sys.getsizeof(a) / 1e6:.1f} МБ")
Стоит отметить, что sys.getsizeof() возвращает размер объекта Python, который включает как блок данных, так и накладные расходы на метаданные. Для мониторинга общего использования памяти процессом можно использовать модуль psutil:

Python
1
2
3
4
5
6
7
8
9
10
11
12
import psutil
 
# Получаем текущее использование памяти
process = psutil.Process()
memory_before = process.memory_info().rss / 1e6  # в МБ
 
# Создаём большой массив
big_array = np.ones(100000000, dtype=np.float64)  # ~800 МБ
 
# Измеряем использование памяти снова
memory_after = process.memory_info().rss / 1e6
print(f"Использовано памяти: {memory_after - memory_before:.1f} МБ")
Эффективное управление памятью в NumPy требует понимания внутреннего устройства массивов и особенностей их поведения. Правильный выбор между представлениями и копиями, оптимальными типами данных и специализированными структурами данных может значительно повысить производительность ваших программ и позволить работать с данными, которые иначе не поместились бы в память.

При работе с действительно большими данными часто приходится применять стратегию "разделяй и властвуй" — обрабатывать данные по частям. Для этого можно использовать итерацию по блокам:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
def process_in_chunks(filename, chunk_size=1000):
# Открываем файл в режиме memory-mapped
array = np.memmap(filename, dtype=np.float32, mode='r')
total_size = len(array)
    
# Обрабатываем по частям
results = []
for i in range(0, total_size, chunk_size):
    chunk = array[i:i+chunk_size].copy()  # Копируем в память
    result = process_chunk(chunk)
    results.append(result)
    
return combine_results(results)

Практические примеры



Рассмотрим наиболее распространённые и интересные примеры использования этой библиотеки в реальных проектах.

Обработка научных данных



Научные исследования часто генерируют огромные массивы данных, полученных с экспериментального оборудования. NumPy идеально подходит для их обработки. Допустим, у нас есть данные с температурных датчиков за длительный период:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np
 
# Имитация данных с температурных датчиков (°C) за месяц
# shape: (30, 24) - 30 дней по 24 часа
temperatures = np.random.normal(loc=20, scale=5, size=(30, 24))
temperatures += 5 * np.sin(np.linspace(0, 2*np.pi, 24))  # Дневной цикл
 
# Базовый анализ
daily_mean = np.mean(temperatures, axis=1)  # Средняя температура за каждый день
daily_range = np.ptp(temperatures, axis=1)  # Диапазон температур за день
hottest_hour = np.argmax(np.mean(temperatures, axis=0))  # Самый тёплый час
 
print(f"Средняя месячная температура: {np.mean(temperatures):.1f}°C")
print(f"Самый тёплый день: {np.argmax(daily_mean) + 1}-й")
print(f"Самый тёплый час суток: {hottest_hour}:00")
Для выявления аномальных значений можно использовать статистические методы:

Python
1
2
3
4
5
6
7
8
9
# Выявление аномалий с помощью Z-оценки
def find_anomalies(data, threshold=3):
    z_scores = (data - np.mean(data)) / np.std(data)
    return np.where(np.abs(z_scores) > threshold)
 
# Найти аномальные температуры
anomaly_days, anomaly_hours = find_anomalies(temperatures)
for day, hour in zip(anomaly_days, anomaly_hours):
    print(f"Аномальная температура: день {day+1}, час {hour}, {temperatures[day, hour]:.1f}°C")
При обработке данных с научного оборудования часто требуется фильтрация сигнала. NumPy и SciPy предоставляют инструменты для этого:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from scipy import signal
 
# Имитация зашумлённых данных
t = np.linspace(0, 10, 1000)
clean_signal = np.sin(t) + 0.5*np.sin(3*t)
noisy_signal = clean_signal + np.random.normal(0, 0.2, 1000)
 
# Применяем фильтр Баттерворта
b, a = signal.butter(3, 0.1)
filtered_signal = signal.filtfilt(b, a, noisy_signal)
 
# Вычисляем ошибку фильтрации
error = np.sum((filtered_signal - clean_signal)**2) / len(clean_signal)
print(f"Среднеквадратичная ошибка фильтрации: {error:.5f}")

Оптимизация алгоритмов машинного обучения



NumPy — основа большинства библиотек машинного обучения. Для иллюстрации реализуем простой алгоритм линейной регрессии с нуля:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Генерируем синтетические данные
n_samples = 100
X = np.random.rand(n_samples, 1) * 10
y = 2 * X.squeeze() + 1 + np.random.randn(n_samples) * 0.5
 
# Добавляем столбец единиц для свободного члена
X_b = np.c_[np.ones(n_samples), X]
 
# Аналитическое решение с использованием нормального уравнения
theta_best = np.linalg.inv(X_b.T @ X_b) @ X_b.T @ y
 
# Прогноз на новых данных
X_new = np.array([[0], [10]])
X_new_b = np.c_[np.ones(2), X_new]
y_predict = X_new_b @ theta_best
 
print(f"Параметры модели: {theta_best}")
print(f"Прогнозы: {y_predict}")
Для градиентного спуска NumPy также предоставляет все необходимые инструменты:

Python
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
# Градиентный спуск для линейной регрессии
def gradient_descent(X, y, theta, learning_rate, n_iterations):
    m = len(y)
    cost_history = np.zeros(n_iterations)
    
    for it in range(n_iterations):
        gradients = 2/m * X.T @ (X @ theta - y)
        theta = theta - learning_rate * gradients
        cost_history[it] = np.sum((X @ theta - y) ** 2) / m
        
    return theta, cost_history
 
# Начальные параметры
theta = np.random.randn(2, 1)
 
# Подготавливаем данные
X = X_b
y = y.reshape(-1, 1)
 
# Выполняем градиентный спуск
learning_rate = 0.01
n_iterations = 1000
theta_optimized, cost_history = gradient_descent(X, y, theta, learning_rate, n_iterations)
 
print(f"Оптимизированные параметры: {theta_optimized.flatten()}")
print(f"Конечная ошибка (MSE): {cost_history[-1]:.5f}")
Для более сложных моделей, таких как нейронные сети, NumPy также может быть использован для реализации вычислений, хотя специализированные библиотеки обычно предлагают более оптимизированные решения.

Визуализация многомерных данных



NumPy прекрасно интегрируется с визуализационными библиотеками, позволяя быстро и эффективно отображать сложные данные:

Python
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
import matplotlib.pyplot as plt
 
# Создаём многомерные данные
n_samples = 500
n_features = 10
X = np.random.randn(n_samples, n_features)
 
# Вычисляем корреляционную матрицу
corr_matrix = np.corrcoef(X.T)
 
# Визуализируем корреляционную матрицу
plt.figure(figsize=(10, 8))
plt.imshow(corr_matrix, cmap='coolwarm', vmin=-1, vmax=1)
plt.colorbar(label='Коэффициент корреляции')
plt.title('Корреляционная матрица признаков')
plt.tight_layout()
 
# Снижение размерности с помощью PCA
U, S, Vh = np.linalg.svd(X, full_matrices=False)
X_reduced = U[:, :2] * S[:2]  # Проекция на первые две главные компоненты
 
# Визуализация данных в пространстве главных компонент
plt.figure(figsize=(10, 8))
plt.scatter(X_reduced[:, 0], X_reduced[:, 1], alpha=0.7)
plt.title('Проекция данных на первые две главные компоненты')
plt.xlabel('Главная компонента 1')
plt.ylabel('Главная компонента 2')
plt.grid(True)
plt.tight_layout()

Практическое применение NumPy в финансовых расчётах



Финансовый анализ требует работы с временными рядами и статистикой, где NumPy очень полезен:

Python
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
30
31
32
33
34
35
36
37
38
39
40
41
42
# Моделирование цены акций с помощью геометрического броуновского движения
def simulate_stock_price(S0, mu, sigma, T, dt, paths):
    steps = int(T / dt)
    price_paths = np.zeros((paths, steps + 1))
    price_paths[:, 0] = S0
    
    for t in range(1, steps + 1):
        z = np.random.standard_normal(paths)
        price_paths[:, t] = price_paths[:, t-1] * np.exp((mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * z)
    
    return price_paths
 
# Параметры
S0 = 100  # Начальная цена
mu = 0.05  # Ожидаемая доходность (годовая)
sigma = 0.2  # Волатильность (годовая)
T = 1.0  # Период в годах
dt = 1/252  # Шаг (1 торговый день)
paths = 1000  # Количество симуляций
 
# Проводим симуляцию
stock_prices = simulate_stock_price(S0, mu, sigma, T, dt, paths)
 
# Анализ результатов
final_prices = stock_prices[:, -1]
mean_price = np.mean(final_prices)
var_95 = np.percentile(final_prices, 5)
var_99 = np.percentile(final_prices, 1)
 
print(f"Начальная цена: ${S0:.2f}")
print(f"Средняя конечная цена: ${mean_price:.2f}")
print(f"Value at Risk (95%): ${S0 - var_95:.2f}")
print(f"Value at Risk (99%): ${S0 - var_99:.2f}")
 
# Визуализация нескольких случайных путей
plt.figure(figsize=(12, 6))
for i in range(10):
    plt.plot(stock_prices[i])
plt.title('Симуляция цены акции (10 случайных путей)')
plt.xlabel('Дни')
plt.ylabel('Цена, $')
plt.tight_layout()
Расчёт доходности портфеля акций также становится простым с NumPy:

Python
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
30
31
32
33
34
35
36
37
# Моделирование портфеля из нескольких акций
n_assets = 5  # Количество активов в портфеле
 
# Средняя доходность активов
mu_vector = np.array([0.05, 0.08, 0.12, 0.07, 0.03])
 
# Матрица ковариаций
sigma_matrix = np.array([
    [0.04, 0.02, 0.01, 0.02, 0.01],
    [0.02, 0.09, 0.03, 0.02, 0.01],
    [0.01, 0.03, 0.16, 0.01, 0.02],
    [0.02, 0.02, 0.01, 0.09, 0.02],
    [0.01, 0.01, 0.02, 0.02, 0.04]
])
 
# Генерация случайных весов в портфеле
n_portfolios = 10000
weights = np.random.random((n_portfolios, n_assets))
weights = weights / np.sum(weights, axis=1)[:, np.newaxis]  # Нормализация
 
# Расчёт ожидаемой доходности и риска для каждого портфеля
portfolio_return = np.sum(weights * mu_vector, axis=1)
portfolio_volatility = np.sqrt(np.diag(weights @ sigma_matrix @ weights.T))
 
# Вычисление коэффициента Шарпа (при безрисковой ставке 1%)
sharpe_ratio = (portfolio_return - 0.01) / portfolio_volatility
 
# Находим оптимальный портфель (с максимальным коэффициентом Шарпа)
max_sharpe_idx = np.argmax(sharpe_ratio)
optimal_weights = weights[max_sharpe_idx]
 
print("Оптимальные веса портфеля:")
for i, w in enumerate(optimal_weights):
    print(f"Актив {i+1}: {w*100:.1f}%")
print(f"Ожидаемая доходность: {portfolio_return[max_sharpe_idx]*100:.2f}%")
print(f"Ожидаемый риск: {portfolio_volatility[max_sharpe_idx]*100:.2f}%")
print(f"Коэффициент Шарпа: {sharpe_ratio[max_sharpe_idx]:.4f}")

Обработка временных рядов с помощью NumPy



NumPy эффективен для анализа временных рядов, например, при прогнозировании продаж:

Python
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Имитация ежедневных продаж за 3 года
days = 3 * 365
t = np.arange(days)
trend = 0.1 * t  # Линейный тренд роста
seasonality = 20 * np.sin(2 * np.pi * t / 365)  # Годовая сезонность
weekly = 10 * np.sin(2 * np.pi * t / 7)  # Недельная сезонность
noise = np.random.normal(0, 10, days)  # Случайный шум
 
sales = 100 + trend + seasonality + weekly + noise
sales = np.maximum(sales, 0)  # Продажи не могут быть отрицательными
 
# Скользящее среднее для выявления тренда
def moving_average(x, window):
    return np.convolve(x, np.ones(window)/window, mode='valid')
 
# Вычисляем 30-дневное скользящее среднее
ma30 = moving_average(sales, 30)
 
# Автокорреляция для выявления сезонности
def autocorrelation(x, max_lag):
    result = np.zeros(max_lag)
    x_mean = np.mean(x)
    x_var = np.var(x)
    
    for lag in range(max_lag):
        result[lag] = np.mean((x[:-lag if lag > 0 else None] - x_mean) * 
                             (x[lag:] - x_mean)) / x_var
                             
    return result
 
# Вычисляем автокорреляцию с лагом до 400 дней
acf = autocorrelation(sales, 400)
 
plt.figure(figsize=(12, 6))
plt.plot(sales, alpha=0.7, label='Ежедневные продажи')
plt.plot(np.arange(29, 29+len(ma30)), ma30, 'r', linewidth=2, label='30-дневное скользящее среднее')
plt.title('Временной ряд продаж')
plt.legend()
plt.tight_layout()
 
plt.figure(figsize=(12, 6))
plt.plot(acf)
plt.axhline(0, linestyle='--', color='gray')
plt.title('Автокорреляционная функция')
plt.xlabel('Лаг (дни)')
plt.ylabel('Автокорреляция')
plt.tight_layout()
Такой анализ позволяет выявить долгосрочные тренды и сезонные компоненты во временных рядах, что критически важно для прогнозирования. Интересно, что при анализе годовых данных о продажах часто обнаруживаются неделные пики, которые легко улавливаются с помощью автокорреляционной функции NumPy. Это хорошо соответствует интуитивному пониманию: люди чаще делают покупки в выходные, что создаёт чёткий семидневный паттерн, заметный при анализе.

Нестандартные решения типичных проблем



При работе с NumPy иногда возникают нестандартные ситуации, для которых требуются особые решения. Рассмотрим наиболее интересные и полезные подходы к решению типичных задач.

Работа с разреженными данными



Когда большинство элементов массива равны нулю, использование стандартных массивов NumPy становится неэффективным. Для таких ситуаций лучше применять разреженные матрицы из модуля SciPy:

Python
1
2
3
4
5
6
7
8
9
10
from scipy import sparse
import numpy as np
 
# Создаём разреженную матрицу
data = np.eye(10000)  # Единичная матрица 10000x10000
# Превращаем в разреженный формат
sparse_data = sparse.csr_matrix(data)  
 
print(f"Размер обычного массива: {data.nbytes / 1024 / 1024:.1f} МБ")
print(f"Размер разреженного массива: {sparse_data.data.nbytes / 1024 / 1024:.1f} МБ")
Основные форматы разреженных матриц в SciPy:
CSR (Compressed Sparse Row) — оптимален для строчных операций
CSC (Compressed Sparse Column) — предпочтителен для столбцовых операций
COO (Coordinate) — удобен для пошагового создания матрицы

Операции с разреженными матрицами часто оказываются не только экономичнее по памяти, но и быстрее для вычислений:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Умножение разреженных матриц
A = sparse.random(10000, 10000, density=0.01)  # 1% ненулевых элементов
B = sparse.random(10000, 10000, density=0.01)
 
# Засекаем время умножения
import time
 
start = time.time()
C_sparse = A @ B  # Операция для разреженных матриц
sparse_time = time.time() - start
 
# Преобразуем в обычные массивы для сравнения
A_dense = A.toarray()
B_dense = B.toarray()
 
start = time.time()
C_dense = A_dense @ B_dense  # Операция для плотных матриц
dense_time = time.time() - start
 
print(f"Разреженное умножение: {sparse_time:.3f} сек")
print(f"Плотное умножение: {dense_time:.3f} сек")

Продвинутые преобразования массивов



Иногда требуется преобразовать данные особым образом, например, превратить массив различных измерений в единую таблицу. Такой подход часто используется при анализе экспериментальных данных:

Python
1
2
3
4
5
6
7
8
# У нас есть массив измерений в разных точках пространства
X, Y = np.meshgrid(np.linspace(-5, 5, 50), np.linspace(-5, 5, 40))
Z = np.sin(np.sqrt(X[B]2 + Y[/B]2))
 
# Преобразуем в таблицу (x, y, z)
points = np.column_stack((X.ravel(), Y.ravel(), Z.ravel()))
print(f"Форма исходных данных: X={X.shape}, Y={Y.shape}, Z={Z.shape}")
print(f"Форма преобразованных данных: {points.shape}")
Для обратного преобразования подобных данных можно использовать триангуляцию и интерполяцию:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from scipy.interpolate import griddata
 
# Имеем неструктурированные точки
n_points = 1000
x = np.random.uniform(-5, 5, n_points)
y = np.random.uniform(-5, 5, n_points)
z = np.sin(np.sqrt(x[B]2 + y[/B]2))
 
# Создаём регулярную сетку
xi = np.linspace(-5, 5, 100)
yi = np.linspace(-5, 5, 100)
xi_grid, yi_grid = np.meshgrid(xi, yi)
 
# Интерполируем значения на регулярную сетку
zi = griddata((x, y), z, (xi_grid, yi_grid), method='cubic')

Использование универсальных функций (ufuncs)



Универсальные функции NumPy — мощный механизм для создания собственных векторизованных операций. С их помощью можно существенно ускорить вычисления:

Python
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
# Определяем свою универсальную функцию
@np.vectorize
def custom_sigmoid(x):
    return 1 / (1 + np.exp(-x))
 
# Применяем к массиву
x = np.linspace(-10, 10, 1000)
y = custom_sigmoid(x)
 
# Сравним с циклом
def sigmoid_loop(arr):
    result = np.zeros_like(arr)
    for i, val in enumerate(arr):
        result[i] = 1 / (1 + np.exp(-val))
    return result
 
# Проверка времени выполнения
start = time.time()
y1 = custom_sigmoid(x)
vec_time = time.time() - start
 
start = time.time()
y2 = sigmoid_loop(x)
loop_time = time.time() - start
 
print(f"Векторизованная версия: {vec_time:.6f} сек")
print(f"Версия с циклом: {loop_time:.6f} сек")
Для ещё большей производительности можно создавать низкоуровневые ufunc'и с помощью Numba:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from numba import vectorize
 
@vectorize(['float64(float64)'], nopython=True)
def fast_sigmoid(x):
    return 1 / (1 + np.exp(-x))
 
# Проверка скорости
start = time.time()
y3 = fast_sigmoid(x)
numba_time = time.time() - start
 
print(f"Numba-версия: {numba_time:.6f} сек")

Интеграция с нативным C/C++ кодом



Для критически важных вычислений иногда требуется прямая интеграция с кодом на C/C++. NumPy предоставляет несколько способов, включая Cython и ctypes:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Пример с использованием ctypes
import ctypes
 
# Предположим, что у нас есть библиотека C с функцией
[H2]double fast_dot(double* a, double* b, int n);[/H2]
 
# Загружаем библиотеку
lib = ctypes.CDLL('./fast_math.so')
 
# Определяем интерфейс функции
lib.fast_dot.argtypes = [
    np.ctypeslib.ndpointer(dtype=np.float64, ndim=1, flags='C_CONTIGUOUS'),
    np.ctypeslib.ndpointer(dtype=np.float64, ndim=1, flags='C_CONTIGUOUS'),
    ctypes.c_int
]
lib.fast_dot.restype = ctypes.c_double
 
# Используем функцию
a = np.random.randn(1000000).astype(np.float64)
b = np.random.randn(1000000).astype(np.float64)
 
# Вызов C-функции
result = lib.fast_dot(a, b, len(a))

Кэширование промежуточных результатов



При многократных вычислениях с одними и теми же данными кэширование может дать существенный прирост производительности:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from functools import lru_cache
 
# Кэшируем результаты вычисления факториала
@lru_cache(maxsize=None)
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n-1)
 
# Для использования с NumPy массивами нужно немного хитрости
def vectorized_factorial(arr):
    # Преобразуем в кортеж, чтобы сделать хешируемым
    if isinstance(arr, np.ndarray):
        results = np.empty_like(arr)
        for i, val in enumerate(arr.flat):
            results.flat[i] = factorial(int(val))
        return results
    return factorial(int(arr))
 
# Проверка
values = np.arange(10)
factorials = vectorized_factorial(values)
print(factorials)
Также для кэширования промежуточных результатов при работе с большими массивами часто используется техника мемоизации:

Python
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
30
31
32
33
34
35
36
37
38
class Memoize:
    def __init__(self, func):
        self.func = func
        self.cache = {}
        
    def __call__(self, *args):
        # Для массивов используем байтовое представление как ключ
        cache_args = tuple(arg.tobytes() if isinstance(arg, np.ndarray) else arg 
                           for arg in args)
        if cache_args in self.cache:
            return self.cache[cache_args]
        else:
            result = self.func(*args)
            self.cache[cache_args] = result
            return result
 
@Memoize
def expensive_computation(data, param):
    # Имитация сложного вычисления
    return np.fft.fft2(data) * param
 
# Тест
data = np.random.randn(1000, 1000)
param = 0.5
 
# Первый вызов (вычисление)
start = time.time()
result1 = expensive_computation(data, param)
first_time = time.time() - start
 
# Второй вызов (из кэша)
start = time.time()
result2 = expensive_computation(data, param)
second_time = time.time() - start
 
print(f"Время первого вызова: {first_time:.3f} сек")
print(f"Время второго вызова: {second_time:.3f} сек")
print(f"Ускорение: {first_time/second_time:.1f}x")
Эти нестандартные подходы значительно расширяют возможности NumPy и позволяют создавать более эффективные решения для сложных задач обработки данных и научных вычислений.

Установка NumPy Python
Приветствую, проблема следующая: установил NumPy через pip и при импорте вылезает ошибка: Traceback (most recent call last): File...

Python Numpy genfromtxt()
Здравствуйте! Есть файл в csv. В задаче указано считывать информацию оттуда с помощью table = np.genfromtxt(’ABBREV.csv’, delimiter=’;’, dtype=None,...

Python - SciPy и NumPy
Написать программу для решения системы из трех линейных алгебраических уравнений (1 ax + 3by + 5z = 18 2. 2ax + 5by + z = 10, 3. 2ax + 3by + 8z = 5)....

Numpy python no attribute 'open'
Добрый день, подскажите пожалуйста что не так, со строкой? opening_quotes = np.array().astype(np.float) import pandas_datareader...

from numpy import array_split -> python
всем привет кто силён в numpy прошу помочь переписать эту функцию на питон from numpy import array_split

Свертка и корреляция в python (numpy)
Свертка и корреляция. - Напишите функцию, которая выполняет свертку. Функция получает изображение и размер ядра, а функция возвращает...

NumPy в Python(float, int)
Всем привет, изучал сегодня NumPy документацию в Python. Знаю, что в этом модуле больше числовых данных чем в самом питоне.С типами int8 ... int64...

Установка numpy для Python 3
Возможно, эта тема уже поднималась на форуме, но вразумительного ответа мне нигде не удалось найти. Я скачивал файлы библиотеки: Пробовал...

Описание функции в python с numpy
Добрый день! Нашел лабораторные задачки, теперь решаю их для себя. Подскажите, как правильно описать функцию (картинка во вложении) в питоне с...

python 2,7 numpy scipy matplotlib
Здравствуйте народ! Только начал изучение python и сразу-же тупик какой-то!!! Хотел сохранить файл после 1 занятия, вышло сообщение такого...

Заполнение (padding) в python (numpy)
Заполнение (padding) : Мне нужно написать функцию заполнения для каждого метода ниже, функция получения изображение и размер ядра, а функция должна...

NumPy для python 3.4 win64
На официальном сайте numpy нашел только версию для 32 разрядной системы? Неужели для 64 разрядной версии не существует? :cry:

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Обнаружение объектов в реальном времени на Python с YOLO и OpenCV
AI_Generated 29.04.2025
Компьютерное зрение — одна из самых динамично развивающихся областей искусственного интеллекта. В нашем мире, где визуальная информация стала доминирующим способом коммуникации, способность машин. . .
Эффективные парсеры и токенизаторы строк на C#
UnmanagedCoder 29.04.2025
Обработка текстовых данных — частая задача в программировании, с которой сталкивается почти каждый разработчик. Парсеры и токенизаторы составляют основу множества современных приложений: от. . .
C++ в XXI веке - Эволюция языка и взгляд Бьярне Страуструпа
bytestream 29.04.2025
C++ существует уже более 45 лет с момента его первоначальной концепции. Как и было задумано, он эволюционировал, отвечая на новые вызовы, но многие разработчики продолжают использовать C++ так, будто. . .
Слабые указатели в Go: управление памятью и предотвращение утечек ресурсов
golander 29.04.2025
Управление памятью — один из краеугольных камней разработки высоконагруженных приложений. Го (Go) занимает уникальную нишу в этом вопросе, предоставляя разработчикам автоматическое управление памятью. . .
Разработка кастомных расширений для компилятора C++
NullReferenced 29.04.2025
Создание кастомных расширений для компиляторов C++ — инструмент оптимизации кода, внедрения новых языковых функций и автоматизации задач. Многие разработчики недооценивают гибкость современных. . .
Гайд по обработке исключений в C#
stackOverflow 29.04.2025
Разработка надёжного программного обеспечения невозможна без грамотной обработки исключительных ситуаций. Любая программа, независимо от её размера и сложности, может столкнуться с непредвиденными. . .
Создаем RESTful API с Laravel
Jason-Webb 28.04.2025
REST (Representational State Transfer) — это архитектурный стиль, который определяет набор принципов для создания веб-сервисов. Этот подход к построению API стал стандартом де-факто в современной. . .
Дженерики в C# - продвинутые техники
stackOverflow 28.04.2025
История дженериков началась с простой идеи — создать механизм для разработки типобезопасного кода без потери производительности. До их появления программисты использовали неуклюжие преобразования. . .
Тестирование в Python: PyTest, Mock и лучшие практики TDD
py-thonny 28.04.2025
Тестирование кода играет весомую роль в жизненном цикле разработки программного обеспечения. Для разработчиков Python существует богатый выбор инструментов, позволяющих создавать надёжные и. . .
Работа с PDF в Java с iText
Javaican 28.04.2025
Среди всех форматов PDF (Portable Document Format) заслуженно занимает особое место. Этот формат, созданный компанией Adobe, превратился в универсальный стандарт для обмена документами, не зависящий. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru