NumPy с нуля: понятный гайд для тех, кто хочет в Data Science

Страницы:  1

Ответить
 

Nemo


Привет, Хабр! Мы все любим Python за его лаконичность и читаемость. Стандартные списки ([font=Courier New]list) в нём — прекрасный и гибкий инструмент. В один список можно закинуть целые числа, строки, словари и даже другие списки. Удобно? Очень.
Но у этой гибкости есть суровая цена — производительность. Когда вы начинаете заниматься анализом данных, машинным обучением или просто сложными вычислениями, где счет идет на миллионы элементов, базовый Python показывает свою слабую сторону. Попытка перемножить два больших списка через стандартный цикл [font=Courier New]for заставит вас успеть заварить кофе (а то и выпить его), пока код выполняется.
Именно здесь на сцену выходит NumPy (Numerical Python) — библиотека, которая де-факто является фундаментом для всей экосистемы Data Science в Python. На ней базируются SciPy, Scikit-learn и, конечно, знаменитый Pandas (кстати, если после прочтения захотите освоить его на практике, заглядывайте на мой бесплатный курс «Pandas для анализа данных: Полный курс»). NumPy написан на языке C и работает с данными на совершенно другом уровне абстракции и скорости.
Что вас ждет в этой статье? Мы не будем лезть в академические дебри. Это практический гайд для первого знакомства. К концу статьи вы:
  • Поймете, как устроены и как работают многомерные массивы.
  • Научитесь элегантно извлекать и менять нужные данные.
  • Познакомитесь с «магией» векторизации.
  • Раз и навсегда отучитесь использовать циклы [font=Courier New]for там, где математика может сделать всё за доли секунды.
2. Зачем нужен NumPy, если есть списки Python?
Казалось бы, зачем учить новый инструмент, если в Python уже есть отличные встроенные списки? Проблема в том, что стандартный [font=Courier New]list создавался для максимальной гибкости, а за гибкость приходится платить ресурсами компьютера.
Давайте разберем три главные причины, почему в Data Science используют именно NumPy.
1. Скорость и то, как данные лежат в памяти
Список в Python — это, по сути, массив указателей. Каждый элемент списка — это отдельный полноценный объект в памяти (со своей метаинформацией, типом и счетчиком ссылок), и эти объекты могут быть разбросаны по оперативной памяти как угодно. Чтобы пройтись по списку, процессору нужно постоянно «прыгать» по памяти, собирая данные.
Массив в NumPy ([font=Courier New]ndarray) устроен иначе. Он хранит данные единым, непрерывным блоком в памяти (как массивы в языке C). Кроме того, все элементы в массиве NumPy имеют один и тот же тип данных. Процессор обожает такую структуру: он может загрузить огромный кусок массива в свой быстрый кэш и обработать его за доли секунды. А еще почти вся математика внутри NumPy написана на высокооптимизированном C, что позволяет обходить медленный интерпретатор Python.
2. Экономия оперативной памяти
Из-за того, что NumPy не хранит служебную информацию (тип объекта, размер и т.д.) для каждого числа, массивы занимают в разы меньше места. Если вы работаете с датасетом на несколько гигабайт, обычные списки Python могут просто «уронить» систему из-за нехватки памяти (OOM), тогда как NumPy спокойно всё переварит.
3. Удобство и векторизация
Чтобы умножить каждый элемент списка на 2, в чистом Python вам придется писать цикл или использовать генератор списков: [font=Courier New][x * 2 for x in my_list].
NumPy вводит понятие векторизации — вы применяете математическую операцию ко всему массиву сразу, как будто это одно число. Никаких циклов писать не нужно, библиотека сделает всё под капотом.
Практика:
Давайте посмотрим на разницу в скорости на простейшем примере. Умножим миллион чисел на 2. Для замера времени в Jupyter Notebook / IPython есть удобная магическая команда [font=Courier New]%timeit.
import numpy as np
# Создаем миллион чисел
size = 1_000_000
python_list = list(range(size))
numpy_array = np.arange(size)
# 1. Классический Python (генератор списков)
%timeit [x * 2 for x in python_list]
# Вывод: 42.1 ms ± 1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# 2. NumPy (векторизация)
%timeit numpy_array * 2
# Вывод: 820 µs ± 15 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Результат: NumPy выполнил ту же самую задачу примерно в 50 раз быстрее (820 микросекунд против 42 миллисекунд). И чем сложнее будут математические формулы, тем больше будет этот разрыв. В мире больших данных эта разница означает выбор между «модель обучится за пару часов» и «мы будем ждать до следующего года».
3. Установка и импорт
Начать работу с NumPy предельно просто. Если библиотека у вас еще не установлена, откройте терминал (или командную строку) и выполните стандартную команду:
pip install numpy
(Примечание: если вы используете сборку Anaconda, то NumPy там уже установлен «из коробки»).
Теперь библиотеку нужно подключить к вашему Python-скрипту или Jupyter Notebook. В мире Data Science существует жесткий, хоть и негласный стандарт того, как именно это делать:
import numpy as np
Почему именно[font=Courier New]np? Технически вы можете написать просто [font=Courier New]import numpy или даже [font=Courier New]import numpy as my_math. Код будет работать абсолютно так же. Но делать так не стоит.
Аббревиатура [font=Courier New]np — это общемировой стандарт. Вы будете обращаться к функциям этой библиотеки сотни раз за день, и сокращение до двух букв банально сэкономит вам время и сделает код визуально чище. К тому же, любой другой разработчик или аналитик, открыв ваш скрипт (или ответ на StackOverflow), моментально поймет, что [font=Courier New]np.array — это вызов метода NumPy. Пишите на общепринятом «диалекте»!
4. Создание массивов: от списков к генераторам
Любая работа в NumPy начинается с создания объекта [font=Courier New]ndarray (n-dimensional array). Есть два основных пути: превратить уже существующий список в массив или сгенерировать его с нуля инструментами самой библиотеки.
Способ 1: Из обычного списка Python
Функция [font=Courier New]np.array() — ваш главный мостик между чистым Python и NumPy.
# Одномерный массив (вектор)
vector = np.array([1, 2, 3])
print(vector)
# Вывод: [1 2 3]
# Двумерный массив (матрица) создается из списка списков
matrix = np.array([[1, 2], [3, 4]])
print(matrix)
# Вывод:
# [[1 2]
# [3 4]]
Небольшая деталь: при выводе массивов NumPy на экран элементы разделяются пробелами, а не запятыми, как в классических списках. Это верный визуальный признак того, что перед вами именно массив NumPy.
Способ 2: Встроенные генераторы (Так делают профи)
В реальных Data Science задачах вы редко будете вбивать числа руками. Обычно вам нужно создать «заготовку» определенного размера или сгенерировать математическую последовательность.
1. Массивы из нулей и единиц Идеально подходят для создания массивов, которые вы планируете заполнять расчетными данными в процессе работы программы.
# Массив из 5 нулей
zeros = np.zeros(5)
print(zeros)
# Вывод: [0. 0. 0. 0. 0.]
# Матрица 2x3 из единиц (обратите внимание: размер передается в двойных скобках, как кортеж!)
ones = np.ones((2, 3))
print(ones)
# Вывод:
# [[1. 1. 1.]
# [1. 1. 1.]]
(Точки после чисел означают, что по умолчанию NumPy создает числа с плавающей точкой —[font=Courier New]float64).
2. Последовательности:[font=Courier New]arangeи[font=Courier New]linspace Если вам нужен аналог стандартного питоновского [font=Courier New]range(), используйте [font=Courier New]np.arange(). Его киллер-фича — умение работать с дробным шагом, чего встроенный [font=Courier New]range() не позволяет.
# От 0 до 10 (не включая 10) с шагом 2
seq = np.arange(0, 10, 2)
print(seq)
# Вывод: [0 2 4 6 8]
# Дробный шаг
float_seq = np.arange(0, 2, 0.5)
print(float_seq)
# Вывод: [0. 0.5 1. 1.5]
Но что, если вам нужно ровно 5 точек на отрезке от 0 до 1, и вам не хочется высчитывать шаг вручную? Для этого (и особенно для построения графиков в будущем) незаменим [font=Courier New]np.linspace():
# 5 равноудаленных точек от 0 до 1 (важно: здесь правая граница включена!)
points = np.linspace(0, 1, 5)
print(points)
# Вывод: [0. 0.25 0.5 0.75 1. ]
3. Случайные числа Внутри NumPy есть мощнейший подмодуль [font=Courier New]random. Он — ваш лучший друг для генерации тестовых данных, симуляций или инициализации весов в нейросетях.
# Массив из 3 случайных чисел в диапазоне [0, 1)
random_arr = np.random.rand(3)
print(random_arr)
# Вывод: [0.5488135 0.71518937 0.60276338]
5. Анатомия массива: как понять, что перед вами?
В реальной работе вы редко будете создавать массивы вручную. Чаще всего вы будете загружать их из файлов (картинки, CSV-таблицы) или получать на выходе из других функций. И первое, что нужно сделать с незнакомым массивом — «осмотреть» его.
У каждого объекта [font=Courier New]ndarray есть встроенные атрибуты (обратите внимание: они вызываются без скобок на конце, так как это свойства, а не функции). Давайте разберем три самых главных.
import numpy as np
# Создадим подопытную матрицу 2x3
matrix = np.array([[1, 2, 3],
[4, 5, 6]])
1. ndim: количество измерений (осей)
Показывает, в каком пространстве живет наш массив.
  • 1 — одномерный массив (вектор/список).
  • 2 — двумерный массив (матрица/таблица).
  • 3 — трехмерный массив (например, цветная картинка, где есть высота, ширина и три цветовых канала RGB).
print(matrix.ndim)
# Вывод: 2 (у нас двумерная матрица)
2. shape: форма массива (Самый важный атрибут!)
Если [font=Courier New]ndim говорит просто «это таблица», то [font=Courier New]shape дает точные габариты. Он возвращает кортеж с размерами массива по каждой оси. Для двумерной матрицы это всегда [font=Courier New](строки, столбцы).
Запомните этот атрибут! 90% ошибок новичков в NumPy и нейросетях связаны с тем, что формы массивов не совпадают при математических операциях (ошибки вида shape mismatch). Проверка [font=Courier New].shape — это ваш главный инструмент отладки.
print(matrix.shape)
# Вывод: (2, 3) — то есть 2 строки и 3 столбца
3. dtype: тип данных
Мы уже упоминали, что массивы NumPy хранятся в памяти единым блоком. Чтобы это работало, все элементы массива должны быть строго одного типа. Вы не можете положить в один массив NumPy число, строку и булево значение, как это позволяет делать обычный [font=Courier New]list.
print(matrix.dtype)
# Вывод: int64 (64-битные целые числа)
А что будет, если попытаться обмануть NumPy? Если вы скормите ему список с разными типами данных, он не выдаст ошибку, но сделает upcasting — приведет все элементы к самому «вместительному» типу (обычно к строкам).
# Пытаемся смешать число 1, число с плавающей точкой и строку
mixed_array = np.array([1, 2.5, "Хабр"])
print(mixed_array)
# Вывод: ['1' '2.5' 'Хабр']
# NumPy превратил все числа в строки, чтобы сохранить однородность!
print(mixed_array.dtype)
# Вывод:
Из-за этого массив потеряет всю свою математическую суперсилу, поэтому за однородностью данных нужно следить.
6. Индексация и срезы: как достать нужные данные
Создать массив — полдела. В реальных задачах (например, при подготовке данных для машинного обучения) вам постоянно придется «вытаскивать» из него определенные строки, столбцы или конкретные значения для анализа.
Одномерные массивы: старая добрая классика
Здесь для вас не будет сюрпризов. Если вы умеете работать со срезами в обычных списках Python, вы уже умеете работать с одномерными массивами NumPy. Индексы начинаются с нуля, срезы работают по правилу [font=Courier New][старт:стоп:шаг].
arr = np.array([10, 20, 30, 40, 50])
print(arr[0]) # Вывод: 10 (первый элемент)
print(arr[-1]) # Вывод: 50 (последний элемент)
print(arr[1:4]) # Вывод: [20 30 40] (срез с 1-го по 3-й индекс включительно)
Многомерные массивы: прощайте, вложенные скобки!
Главное отличие начинается, когда мы переходим к матрицам. В классическом Python, чтобы достать элемент из списка списков, вы бы написали так: [font=Courier New]matrix[0][1]. В NumPy это считается устаревшим подходом и работает медленнее.
NumPy использует элегантный синтаксис через запятую: [font=Courier New][строка, столбец].
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# Достаем число 6 (индекс строки 1, индекс столбца 2)
print(matrix[1, 2])
# Вывод: 6
Мощь срезов в 2D: Настоящая сила синтаксиса с запятой раскрывается в срезах. Представьте, что это таблица (например, выгрузка из базы данных), и вам нужно забрать весь первый столбец (скажем, только имена пользователей). В чистом Python пришлось бы писать цикл. В NumPy двоеточие [font=Courier New]: означает «взять всё по этой оси».
# Берем ВСЕ строки ( , но только нулевой столбец (0)
first_column = matrix[:, 0]
print(first_column)
# Вывод: [1 4 7]
# Берем подматрицу (первые две строки и последние два столбца)
sub_matrix = matrix[0:2, 1:3]
print(sub_matrix)
# Вывод:
# [[2 3]
# [5 6]]
Булева индексация
А теперь пристегнитесь, мы переходим к настоящей магии. Допустим, у вас есть массив с тысячей чисел, и вам нужно выбрать только те, которые больше 50.
В обычном Python вы бы написали что-то вроде [font=Courier New][x for x in data if x > 50]. NumPy позволяет делать это без циклов, невероятно быстро и лаконично, с помощью так называемых «масок». Вы можете передать условие прямо в квадратные скобки.
data = np.array([12, 85, 34, 99, 5, 67, 42])
# Сначала посмотрим, что делает само условие
print(data > 50)
# Вывод: [False True False True False True False]
# (NumPy вернул массив из True/False для каждого элемента)
# А теперь передаем это условие внутрь квадратных скобок в качестве индекса!
result = data[data > 50]
print(result)
# Вывод: [85 99 67]
Почему это вызывает восторг? Потому что вы можете легко комбинировать условия фильтрации данных! Нужно найти числа больше 20, но меньше 50? Легко. Главное — использовать побитовые операторы [font=Courier New]& (и) или [font=Courier New]| (или) вместо классических питоновских [font=Courier New]and/[font=Courier New]or и обязательно брать условия в круглые скобки:
# Числа от 20 до 50
print(data[(data > 20) & (data < 50)])
# Вывод: [34 42]
Это не просто короче в написании. Под капотом фильтрация происходит на уровне языка C, что критически важно при обработке датасетов на миллионы строк. Именно на этой механике строится львиная доля фильтрации данных в библиотеке Pandas!
7. Базовая математика и Векторизация
Мы подошли к главной причине, по которой NumPy стал стандартом де-факто в вычислениях. Это векторизация — способность применять математические операции сразу ко всему массиву, не прибегая к медленным циклам [font=Courier New]for уровня Python.
Операции с числами (скалярами)
Представьте, что у вас есть массив цен в магазине, и вам нужно добавить к каждой цене 100 рублей. В стандартном Python пришлось бы писать цикл или генератор. В NumPy вы просто прибавляете число к массиву, как будто это единичный объект.
prices = np.array([150, 300, 450, 1000])
# Прибавляем 100 к каждому элементу
new_prices = prices + 100
print(new_prices)
# Вывод: [ 250 400 550 1100]
# Можно использовать любые операторы: -, *, /, **, //
discounted = prices * 0.8 # скидка 20%
print(discounted)
# Вывод: [120. 240. 360. 800.]
Код читается как обычное предложение на английском языке, а выполняется на скоростях языка C.
Операции между массивами
Если у вас есть два массива одинаковой формы, вы можете применять арифметику прямо к ним. Операции будут выполняться строго поэлементно (первый элемент с первым, второй со вторым и т.д.).
revenue = np.array([1000, 2500, 3000]) # Выручка
costs = np.array([400, 900, 1200]) # Затраты
# Считаем чистую прибыль
profit = revenue - costs
print(profit)
# Вывод: [ 600 1600 1800]
Важное замечание для тех, кто помнит линейную алгебру: Символ [font=Courier New]* в NumPy означает именно поэлементное умножение, а не классическое матричное (строка на столбец). Если вам вдруг понадобится умножить матрицы по правилам линейной алгебры, для этого используется оператор [font=Courier New]@ (или функция [font=Courier New]np.dot()).
Broadcasting (Транслирование)
Возникает логичный вопрос: «А что будет, если попытаться сложить массивы разной формы?».
По умолчанию математика работает только для массивов одинакового размера. Но в NumPy есть Broadcasting (транслирование). Если формы массивов не совпадают, NumPy попытается «растянуть» (продублировать) меньший массив так, чтобы он совпал по размерам с большим.
На самом деле, вы уже видели Broadcasting в действии минуту назад! Когда мы писали [font=Courier New]prices + 100, NumPy виртуально «растянул» число 100 в массив [font=Courier New][100, 100, 100, 100], и только потом выполнил сложение. Это работает и для более сложных структур (например, когда к матрице прибавляют одномерный вектор), что позволяет писать невероятно компактный код без лишних выделений памяти.
8. Изменение формы (Reshape)
В мире данных информация редко приходит к нам в идеальном виде. Часто бывает так, что вы загружаете изображение, а библиотека считывает его как длинную одномерную «колбасу» из пикселей, хотя для работы вам нужна двумерная матрица (высота и ширина).
Здесь на помощь приходит метод [font=Courier New].reshape(). Он позволяет перестраивать геометрию массива на лету, не меняя и не копируя сами данные в памяти. Главное (и единственное) правило — общее количество элементов должно оставаться неизменным.
Как свернуть вектор в матрицу
Давайте возьмем одномерный массив из 12 чисел и превратим его в матрицу 3х4 (3 строки, 4 столбца).
# Создаем одномерный массив от 0 до 11
arr = np.arange(12)
print(arr)
# Вывод: [ 0 1 2 3 4 5 6 7 8 9 10 11]
# Меняем форму на 3 строки и 4 столбца
matrix = arr.reshape(3, 4)
print(matrix)
# Вывод:
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
Если вы попытаетесь сделать [font=Courier New].reshape(3, 5) из 12 элементов, NumPy справедливо выдаст ошибку [font=Courier New]ValueError. Математика неумолима: 3 умножить на 5 будет 15, а у нас в наличии только 12 чисел. Нехватка или переизбыток данных не допускается.
Лайфхак с минус единицей (-1)
А теперь представьте реальную задачу из машинного обучения. У вас есть огромный массив данных, и вам нужно переформатировать его так, чтобы получилось ровно 2 столбца. Вы совершенно не хотите (или не можете, если размер датасета меняется) вручную делить общее количество элементов на 2, чтобы узнать количество строк.
Для этого в NumPy есть гениальный трюк: вместо одного из измерений вы можете передать [font=Courier New]-1. Это сигнал для NumPy: «Я точно знаю, что столбцов должно быть 2, а количество строк посчитай за меня, исходя из длины массива».
# Просим сделать 2 столбца, а строки (-1) пусть NumPy высчитывает сам
auto_matrix = arr.reshape(-1, 2)
print(auto_matrix)
# Вывод:
# [[ 0 1]
# [ 2 3]
# [ 4 5]
# [ 6 7]
# [ 8 9]
# [10 11]]
# Проверяем форму — NumPy сам понял, что строк должно быть 6
print(auto_matrix.shape)
# Вывод: (6, 2)
Этот лайфхак с [font=Courier New]-1 сэкономит вам уйму времени и нервов при подготовке данных (особенно когда вы начнете вытягивать массивы признаков перед подачей их в модели Scikit-learn или слои нейросетей). Вы можете использовать [font=Courier New]-1 на любой позиции, но, разумеется, только один раз за один вызов [font=Courier New]reshape (иначе уравнение будет иметь бесконечное множество решений).
9. Статистика и Агрегация: сводим данные воедино
Обычно мы загружаем данные в массивы не просто так, а чтобы найти в них какие-то закономерности. Кто потратил больше всего денег? Каков средний чек? Сколько всего товаров продано?
В чистом Python для этого пришлось бы писать циклы с кучей промежуточных переменных-счетчиков. NumPy позволяет получить базовую статистику вызовом одного метода.
Глобальная статистика (по всему массиву)
Если вызвать методы агрегации без аргументов, NumPy схлопнет весь массив (даже если он многомерный) до одного числа.
sales = np.array([[10, 20, 30],
[40, 50, 60]])
print(sales.sum()) # Сумма всех элементов: 210
print(sales.mean()) # Среднее арифметическое: 35.0
print(sales.max()) # Максимальное значение: 60
print(sales.min()) # Минимальное значение: 10
Магия осей: axis=0 и axis=1
А что, если наша таблица [font=Courier New]sales — это продажи трех разных магазинов (столбцы) за два дня (строки)? Нам вряд ли нужна общая сумма. Мы захотим узнать либо выручку за каждый день, либо общую выручку каждого магазина.
Для этого в методы агрегации передается параметр [font=Courier New]axis (ось). Это самая частая точка путаницы для новичков, поэтому давайте разберем её визуально.
Представьте двумерную матрицу. У неё есть два направления движения:
  • [font=Courier New]axis=0 — движение сверху вниз (по столбцам).
  • [font=Courier New]axis=1 — движение слева направо (по строкам).
axis=1 (по строкам →)
[ 10, 20, 30 ]
↓ [ 40, 50, 60 ]
axis=0
(по столбцам)
Правило большого пальца: параметр [font=Courier New]axis указывает, какую ось мы схлопываем (уничтожаем), чтобы получить результат.
  • Если мы хотим посчитать сумму продаж каждого магазина (сумма по столбцам), мы должны «схлопнуть» строки. Двигаемся сверху вниз — это [font=Courier New]axis=0.
# Сумма по вертикали (по столбцам)
print(sales.sum(axis=0))
# Вывод: [50 70 90]
# (10+40, 20+50, 30+60)
  • Если мы хотим посчитать сумму продаж за каждый день (сумма по строкам), мы «схлопываем» столбцы. Двигаемся слева направо — это [font=Courier New]axis=1.
# Сумма по горизонтали (по строкам)
print(sales.sum(axis=1))
# Вывод: [ 60 150]
# (10+20+30, 40+50+60)
Это правило работает абсолютно для всех статистических функций NumPy, а в будущем оно перейдет вместе с вами и в библиотеку Pandas, где логика [font=Courier New]axis=0 и [font=Courier New]axis=1 точно такая же.
10. Заключение: фундамент заложен
Вот мы и разобрали базу NumPy. Мы прошли путь от медленной «черепахи» классических списков до скорости оптимизированного C-кода. Кратко резюмируем, что теперь есть в вашем арсенале. Вы умеете:
  • Быстро создавать массивы нужного размера с помощью генераторов ([font=Courier New]np.zeros, [font=Courier New]np.arange).
  • Понимать структуру незнакомых данных с помощью главного атрибута [font=Courier New].shape.
  • Элегантно нарезать матрицы и фильтровать данные по сложным условиям с помощью булевых масок
  • Менять геометрию данных на лету через [font=Courier New].reshape() и лайфхак с [font=Courier New]-1.
  • Использовать векторизацию и собирать статистику по нужным осям ([font=Courier New]axis) — и всё это без единого цикла [font=Courier New]for!
Что дальше? Куда развиваться?
NumPy — это невероятно мощный, но всё-таки низкоуровневый математический инструмент. Если вы целитесь в Data Science или аналитику, ваш следующий логичный шаг — библиотека Pandas. Она целиком построена поверх NumPy и превращает «голые» массивы в удобные таблицы (DataFrames) с названиями колонок и индексов. Это как Excel на стероидах для программистов.
Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.
А чтобы сухие цифры превращались в понятные инсайты, обязательно познакомьтесь с библиотеками для визуализации, такими как Matplotlib или Seaborn. Они бесшовно работают с массивами NumPy и позволяют строить графики любой сложности буквально в пару строк кода.-Источник
 
Loading...
Error