Python для AI: Экосистема и структуры данных

Погружение в Python через призму Senior Go-разработчика. Освоение NumPy и Pandas с использованием продвинутого Vibecoding для подготовки данных к машинному обучению.

Python глазами Gopher'а: типизация, управление памятью и интерпретируемая среда

Python глазами Gopher'а: типизация, управление памятью и интерпретируемая среда

Парадокс: вы — Senior Go-разработчик. Вы привыкли к бинарникам, компилируемым за миллисекунды, легковесным горутинам и строгой статической типизации. И теперь, переходя в AI-инженерию, вы видите, что вся индустрия машинного обучения построена на Python — языке, который интерпретируется на лету, не имеет строгих типов и блокирует параллельное выполнение потоков. Почему самая вычислительно сложная отрасль в мире выбрала один из самых медленных языков?

Ответ кроется в смене парадигмы. В Go язык — это инструмент для написания бизнес-логики и высоконагруженных систем. В AI Python — это не инструмент вычислений. Это высокоуровневый клей (API), который управляет тяжеловесными C/C++ и CUDA-библиотеками.

Чтобы эффективно применять Vibecoding и управлять AI-агентами при написании Python-кода, вам нужно откалибровать свою ментальную модель и понять, как Python работает «под капотом».

От бинарника к виртуальной машине (PVM)

В Go процесс сборки прямолинеен: go build берет исходный код и компилирует его в машинный код под конкретную архитектуру. Вы получаете один исполняемый файл, который ОС запускает напрямую.

Python работает иначе. При запуске скрипта исходный код сначала транслируется в байт-код (вы могли видеть папки __pycache__ и файлы .pyc). Затем этот байт-код исполняется Python Virtual Machine (PVM) — обычно это стандартная реализация CPython.

PVM — это огромный цикл на языке C, который читает инструкции байт-кода и выполняет соответствующие им C-функции.

В мире AI Python-код — это просто пульт управления. Когда вы пишете код для обучения нейросети, 99% времени процессора или видеокарты тратится не в PVM, а внутри предкомпилированных C-библиотек, к которым PVM просто обращается через интерфейс (FFI).

Concurrency и проклятие GIL

В Go многопоточность встроена в ДНК языка. Планировщик Go распределяет десятки тысяч горутин по NN системным потокам (модель M:N), позволяя им выполняться параллельно на всех ядрах процессора.

В CPython существует механизм, который вызывает наибольший шок у разработчиков из других экосистем — Global Interpreter Lock (GIL).

GIL — это глобальный мьютекс. Он гарантирует, что в любой момент времени только один системный поток может выполнять байт-код Python. Даже если у вас 32-ядерный сервер и вы запустили 32 потока (через модуль threading), они будут выстраиваться в очередь, передавая друг другу GIL.

Кажется, что для машинного обучения, где требуются колоссальные параллельные вычисления, это приговор. Но здесь срабатывает архитектурный трюк экосистемы Python: GIL защищает только внутренние структуры данных самого интерпретатора.

Когда Python вызывает тяжелую функцию из библиотеки, написанной на C (например, перемножение матриц в NumPy или расчет градиентов в PyTorch), эта библиотека отпускает GIL перед началом вычислений. Вся тяжелая математика выполняется на C или CUDA параллельно на всех доступных ядрах или тысячах ядер GPU. В это время PVM может спокойно передать GIL другому Python-потоку для выполнения легковесных задач (например, чтения следующего батча данных с диска).

Типизация: от интерфейсов к Duck Typing

В Go типизация статическая. Интерфейсы реализуются неявно, но компилятор жестко проверяет соответствие типов на этапе сборки. Если функция ждет io.Reader, вы не скомпилируете код, передав туда int.

Python использует динамическую строгую типизацию. Тип привязан к объекту в памяти, а не к переменной. Переменная — это просто ссылка (указатель). Философия полиморфизма в Python строится на Duck Typing (утиной типизации):

«Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка».

Если функция вызывает метод .read() у переданного объекта, Python не волнует, какой класс у этого объекта. Если метод существует — код выполнится. Если нет — упадет с AttributeError в рантайме (во время выполнения).

Для Senior-разработчика падение в рантайме из-за неверного типа — это неприемлемый риск. Именно поэтому в современном Python (и особенно при Vibecoding, как мы разбирали в первом курсе) мы используем Type Hints (аннотации типов) и библиотеки вроде Pydantic.

# Классический Python (опасно для сложных систем)
def process_data(data):
    return data.sum()

# Современный Python (контракты для AI-агентов и статических анализаторов)
from typing import List

def process_data(data: List[float]) -> float:
    return sum(data)

Аннотации типов не меняют поведение PVM — интерпретатор их игнорирует. Но они служат жестким контрактом для статических анализаторов (например, mypy) и, что критически важно, для LLM. Когда вы просите Claude Code написать интеграцию, наличие Type Hints задает границы, не позволяя ИИ галлюцинировать с типами возвращаемых значений.

Управление памятью: Reference Counting вместо GC-пауз

В Go работает конкурентный сборщик мусора (Garbage Collector) алгоритма Mark-and-Sweep. Он периодически сканирует память, строит граф достижимых объектов, а затем удаляет всё, до чего не смог добраться. Это вызывает небольшие задержки (STW - Stop The World), которые команда Go годами оптимизировала до долей миллисекунды.

В Python основной механизм управления памятью совершенно другой — Reference Counting (подсчет ссылок).

Каждый объект в Python имеет скрытое поле — счетчик ссылок.

  1. Создали список a = [1, 2] → счетчик равен 1.
  2. Передали его в функцию process(a) → внутри функции счетчик стал 2.
  3. Функция завершилась → счетчик вернулся к 1.
  4. Вызвали del a или переменная вышла из области видимости → счетчик стал 0.

Как только счетчик достигает нуля, память освобождается моментально, детерминированно и без ожидания прохода сборщика мусора. Для AI это невероятно полезно: когда вы удаляете огромную матрицу на 10 ГБ из оперативной памяти или памяти GPU, ресурсы освобождаются в ту же микросекунду.

Но у подсчета ссылок есть фатальный недостаток — циклические зависимости.

Если объект A ссылается на объект B, а объект B ссылается на объект A, их счетчики ссылок никогда не опустятся ниже 1, даже если основная программа потеряла к ним доступ. Чтобы решать эту проблему, в Python есть дополнительный сборщик мусора (Generational GC), который периодически просыпается, ищет такие изолированные «острова» объектов и удаляет их.

Понимая эти отличия, вы перестаете бороться с Python и начинаете использовать его по назначению: как гибкий и выразительный язык управления контрактами, делегирующий тяжелую работу специализированным C-движкам. Именно с таким движком мы познакомимся на следующем шаге.

Векторизация против циклов: NumPy как фундамент высокопроизводительных вычислений в AI

Векторизация против циклов: NumPy как фундамент высокопроизводительных вычислений в AI

Если вы напишете стандартный цикл for в Go для умножения 10 миллионов чисел, это займет миллисекунды. Если вы напишете идентичный цикл в чистом Python, вы будете ждать секунды. Разница в производительности достигает 100-кратного размера. Для AI-инженера, работающего с датасетами на гигабайты, это означает выбор между «модель обучается час» и «модель обучается неделю».

В прошлой главе мы выяснили причину: PVM (Python Virtual Machine) выполняет байт-код с накладными расходами на проверку типов (Duck Typing) и подсчет ссылок (Reference Counting) на каждой итерации. Чтобы писать высокопроизводительный код на Python, нужно парадоксальным образом перестать писать циклы на Python.

Здесь на сцену выходит NumPy — библиотека, которая превращает интерпретируемый Python в интерфейс для управления скомпилированным C-кодом.

Анатомия памяти: PyObject против сырых байтов

В Go срез (slice) []float64 — это непрерывный блок памяти, хранящий сырые 64-битные числа. Процессор загружает их в кэш линиями и обрабатывает максимально эффективно.

Стандартный список в Python (list) устроен иначе. Это массив указателей, каждый из которых ссылается на отдельный PyObject, разбросанный в куче (heap). Каждый PyObject хранит не только само значение, но и счетчик ссылок, и информацию о типе.

NumPy вводит структуру ndarray (N-dimensional array). Под капотом это тот же непрерывный блок однородной памяти, что и в Go или C.

Характеристика Python list Go []float64 NumPy ndarray
Расположение в памяти Массив указателей на разрозненные объекты Непрерывный блок значений Непрерывный блок значений
Типизация элементов Динамическая (каждый элемент может быть своего типа) Статическая (строго один тип) Статическая (строго один тип, например float64)
Накладные расходы на элемент Высокие (заголовок объекта, счетчик ссылок) Отсутствуют Отсутствуют

Когда LLM генерирует для вас код подготовки данных, критически важно, чтобы она использовала ndarray, а не стандартные списки. Иначе вы потеряете главное преимущество экосистемы — векторизацию.

Векторизация: делегирование циклов на уровень C

Векторизация — это отказ от явных циклов for в Python в пользу операций над целыми массивами.

Рассмотрим задачу: нужно умножить каждый элемент массива на 2.

Подход Junior-разработчика (чистый Python): result = [x * 2 for x in data]

Подход Senior AI-инженера (NumPy): result = data * 2

Во втором случае цикл не исчезает, он опускается на уровень C-движка NumPy. Поскольку ndarray гарантирует, что все элементы имеют одинаковый тип и лежат в памяти непрерывно, C-код освобождает GIL (Global Interpreter Lock), отключает проверки типов для каждого элемента и использует инструкции SIMD (Single Instruction, Multiple Data) на уровне процессора. Процессор умножает сразу блок чисел за один такт.

Broadcasting: магия размерностей без циклов

Векторизация отлично работает, когда массивы одинакового размера: A + B просто сложит элементы попарно. Но в машинном обучении постоянно возникает задача взаимодействия массивов разных размерностей.

Классический пример из нейросетей: у вас есть батч данных (матрица 100 строк на 3 колонки), и к каждой строке нужно прибавить вектор смещения (bias) из 3 элементов.

В Go вы бы написали вложенный цикл. В NumPy вы пишете просто batch + bias.

Это работает благодаря механизму Broadcasting (транслирование). NumPy автоматически «растягивает» меньший массив до размеров большего, не копируя при этом данные в памяти, а лишь меняя параметры итерации (strides) на уровне C.

Правило Broadcasting звучит так: NumPy сравнивает размерности массивов справа налево. Две размерности совместимы, если они равны, либо одна из них равна 1.

Если у вас матрица 100×3100 \times 3 и вектор длины 33 (что при транслировании интерпретируется как 1×31 \times 3), при сравнении справа налево:

  • Последние измерения: 3 и 3 (совпадают).
  • Предпоследние измерения: 100 и 1 (единица «растягивается» до 100). Операция валидна.

Vibecoding: Контракты для LLM при работе с данными

Понимая устройство NumPy, вы меняете подход к генерации кода. Когда вы просите Claude Code или Cursor написать пайплайн обработки данных, вы должны задавать жесткие архитектурные ограничения, опираясь на векторизацию.

Плохой промпт:

Напиши скрипт, который читает данные о пользователях и нормализует их возраст (вычитает среднее и делит на стандартное отклонение).

Claude может сгенерировать решение через встроенный модуль csv и списковые включения (list comprehensions). Это будет работать на 1000 строк и упадет с нехваткой памяти или таймаутом на 10 миллионах.

Хороший промпт от Senior-разработчика:

Напиши функцию нормализации возраста. Ограничения:

  • На вход подается только numpy ndarray типа float32.
  • ЗАПРЕЩЕНО использовать циклы for/while и списковые включения.
  • Вся математика должна быть строго векторизована через методы numpy.
  • Ожидаю O(1) аллокаций памяти в Python-пространстве.

Устанавливая такие контракты, вы используете LLM не просто как генератор текста, а как транслятор ваших архитектурных требований в высокопроизводительный C-подобный код.

Векторизация и Broadcasting в NumPy — это низкоуровневый фундамент. Однако в реальных задачах данные редко приходят в виде идеальных матриц. Обычно это таблицы с пропусками, разными типами колонок и датами. Для управления такими гетерогенными структурами поверх NumPy была создана библиотека, ставшая стандартом индустрии, которую мы разберем на следующем шаге.

Манипуляция данными с Pandas: DataFrame как универсальный контракт для датасетов

Манипуляция данными с Pandas: DataFrame как универсальный контракт для датасетов

NumPy идеально справляется с однородными тензорами, но реальный мир состоит не только из чисел с плавающей точкой. Логи серверов, профили пользователей, финансовые транзакции — это хаотичная смесь строк, дат, булевых флагов и пропусков. Если вы попытаетесь загрузить таблицу с профилями пользователей в строгий ndarray, NumPy приведет все к единому типу (скорее всего, к строкам), убив любую возможность математических вычислений.

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

От среза структур к колоночной памяти

В Go разработчик привык моделировать табличные данные как срез структур: []User. Это построчная (row-based) организация памяти. Данные одного объекта лежат в памяти рядом. Это удобно для обработки конкретного пользователя (например, при отправке HTTP-ответа), но крайне неэффективно для аналитики.

Pandas использует колоночную (columnar) архитектуру. Главный объект библиотеки — DataFrame. Под капотом это не список строк, а словарь (хэш-мапа), где ключи — это названия колонок, а значения — одномерные массивы NumPy (в терминологии Pandas они называются Series).

DataFrame — это набор строго типизированных массивов NumPy, объединенных общим индексом.

Если у вас есть таблица размером 1000×51000 \times 5 (тысяча строк, пять колонок), в памяти это хранится как пять независимых массивов по 1000 элементов.

Почему это критически важно для AI-инженера:

  1. Типизация на уровне колонки: Колонка age может быть массивом целых чисел, а колонка email — массивом указателей на строки. Векторные операции NumPy применяются к колонке age на полной скорости C-движка, не спотыкаясь о текстовые данные из соседних колонок.
  2. Локальность данных: Если ML-модели для обучения нужны только колонки age и salary, Pandas извлечет только два массива. При построчной организации процессору пришлось бы прочитать всю таблицу, отбрасывая ненужные поля на лету.

Индекс: скрытое измерение

Второе фундаментальное отличие DataFrame от простых массивов — наличие Индекса (Index).

В NumPy элементы идентифицируются только их позицией (смещением в памяти). В Pandas каждая строка имеет явную метку (label). По умолчанию это просто числа от 0 до N, но индексом могут быть даты (для временных рядов), UUID пользователей или email-адреса.

Индекс позволяет выполнять автоматическое выравнивание данных (Data Alignment). Если вы попытаетесь сложить две колонки из разных таблиц, Pandas сложит не элементы на одинаковых позициях (как сделал бы NumPy), а элементы с одинаковыми индексами.

Это избавляет от необходимости писать сложную логику синхронизации массивов при слиянии данных из разных источников (например, логов кликов и базы транзакций) — вы просто используете метод merge или join, и Pandas сам сопоставляет строки как в SQL.

DataFrame как контракт для Vibecoding

Pandas обладает колоссальным API: сотни методов для группировки (groupby), сводных таблиц (pivot), оконных функций и работы со временем. Синтаксис местами исторически непоследователен.

Пытаться выучить API Pandas наизусть — пустая трата времени для Senior-разработчика. Это идеальная зона для делегирования Claude Code. Однако, чтобы ИИ сгенерировал надежный код трансформации данных, вы должны управлять процессом через контракты.

В контексте подготовки датасетов, контракт — это схема DataFrame. Вы не описываете ИИ шаги («сначала удали пустые строки, потом сгруппируй по дате»). Вы описываете состояние входа и желаемое состояние выхода.

Пример эффективного системного промпта для трансформации:

У меня есть DataFrame df_raw со схемой: [user_id: string, timestamp: datetime, action: string, amount: float64]. Мне нужен DataFrame df_features со схемой: [user_id: string, total_amount_7d: float64, is_active: bool]. Напиши функцию extract_features(df_raw), используя только векторизованные методы Pandas. Не используй циклы.

Определив типы данных колонок на входе и выходе, вы создаете жесткий интерфейс. Это тот самый подход оркестрации, который мы обсуждали ранее: вы проектируете преобразование типов, а ИИ пишет реализацию groupby и агрегаций.

Главный антипаттерн: побег из C-движка

Самая частая и разрушительная ошибка бэкендеров при знакомстве с Pandas — попытка итерироваться по DataFrame как по коллекции.

Когда вы используете методы вроде iterrows() или обращаетесь к строкам в цикле через df.iloc[i], происходит катастрофа производительности. На каждой итерации Pandas вынужден:

  1. Взять элементы из разных колоночных массивов NumPy.
  2. Собрать их в единый Python-объект (Series или словарь).
  3. Передать этот объект в интерпретатор Python, активируя GIL.

Код, который должен был выполниться за миллисекунды на уровне C, начинает работать минуты, упираясь в ограничения интерпретируемой среды.

Вместо построчной обработки необходимо использовать векторизацию (которую мы разобрали в прошлой главе) или метод apply() в крайних случаях. Если вам нужно создать колонку is_adult, вы не пишете цикл с if age >= 18. Вы пишете df['is_adult'] = df['age'] >= 18. Pandas передаст эту операцию в NumPy, который применит логическое условие ко всему массиву памяти разом.

Обработка пустот: NaN как вирус

Реальные данные всегда содержат пропуски. В SQL для этого есть NULL. В Pandas исторически используется значение NaN (Not a Number) из стандарта IEEE 754 для чисел с плавающей точкой.

Особенность NaN в том, что это значение имеет тип float. Если в колонке с целыми числами (например, количество кликов) появляется хотя бы один пропуск, Pandas автоматически приведет всю колонку к типу float64.

Это поведение часто ломает контракты данных. Вы ожидаете int, передаете тензор в нейросеть, а она падает из-за несовпадения типов. Поэтому очистка данных (Data Cleaning) — вызовы методов dropna() (удалить строки с пропусками) или fillna(0) (заполнить нулями) — это обязательный этап, который должен быть явно прописан в ваших промптах при генерации пайплайнов подготовки данных.

Понимание того, как DataFrame хранит данные в памяти и как выравнивает их по индексам, дает вам полное представление о том, что происходит с вашими датасетами до того, как они превратятся в чистые тензоры. Дальше мы применим эти знания на практике, создав автоматизированный конвейер обработки данных с помощью агентов.

Продвинутый Vibecoding: оркестрация Claude Code для сложной трансформации данных

Продвинутый Vibecoding: оркестрация Claude Code для сложной трансформации данных

Представьте, что у вас есть сырой лог пользовательских действий на 5 гигабайт. Вы открываете терминал, запускаете Claude Code и пишете: «Очисти этот датасет от мусора, сгруппируй по пользователям и подготовь фичи для машинного обучения». Агент радостно принимает задачу, генерирует 300 строк кода на Pandas, запускает их... и падает с ошибкой нехватки памяти (OOM) или выдает таблицу, состоящую сплошь из NaN.

Почему опыт работы с микросервисами на Go здесь не сработал? Потому что вы попытались делегировать ИИ создание монолита. В инженерии данных, особенно при работе с LLM, трансформация должна строиться как направленный ациклический граф (DAG) строгих контрактов.

От монолитных промптов к конвейеру контрактов

В бэкенде вы не пишете одну функцию, которая делает HTTP-запрос, парсит JSON, пишет в базу и отправляет email. Вы разделяете логику интерфейсами. При Vibecoding трансформации данных роль таких интерфейсов играют схемы DataFrame, которые мы разбирали ранее.

LLM великолепно пишет код для перехода из состояния А в состояние Б, если оба состояния четко описаны. Если же вы просите перейти из сырого состояния А в финальное состояние Я, модель начинает галлюцинировать промежуточные колонки, путает индексы и забывает про правила выравнивания данных (Data Alignment).

Решение — оркестрация через промежуточные схемы. Вы не просите Claude Code «подготовить фичи». Вы ставите цепочку задач:

  1. Шаг 1 (Очистка): На входе сырой CSV. На выходе — DataFrame, где колонка price имеет тип float64, а пропуски заполнены медианой.
  2. Шаг 2 (Обогащение): На входе очищенный DataFrame. На выходе — он же, но с новой колонкой is_active типа bool.
  3. Шаг 3 (Агрегация): На входе обогащенный DataFrame. На выходе — агрегированный DataFrame, где индекс — это user_id, а колонки — суммы покупок.

Test-Driven Data Vibecoding (TDDV)

Как заставить автономного агента вроде Claude Code (CLI) следовать этим шагам и не ломать предыдущие этапы при написании следующих? Использовать подход, при котором тесты пишутся до кода трансформации.

В Go вы привыкли к строгой типизации. В Python ее нет, но мы можем эмулировать ее для LLM через связку pytest и схем pandas.

Вместо того чтобы просить Claude сразу писать код обработки, ваш системный промпт (или инструкция в .cursorrules) должен задавать следующий агентный цикл (Agentic Loop):

  1. Генерация фикстур: Агент создает маленький mock_input_df и ожидаемый expected_output_df на основе ваших текстовых контрактов.
  2. Генерация теста: Агент пишет тест, который вызывает будущую функцию трансформации и сравнивает результат с expected_output_df (используя pd.testing.assert_frame_equal).
  3. Генерация логики: Только после того как тест написан и ожидаемо упал, агент пишет векторизованный код на Pandas.
  4. Самоисправление: Если тест падает (например, из-за появления NaN и неявного приведения int к float64), агент видит Stack Trace в терминале и сам корректирует логику.

Пример контракта для промпта

Вот как выглядит идеальная постановка задачи для первого шага трансформации:

Контекст: Мы пишем модуль features.py. Входной контракт: DataFrame df_raw. Колонки: user_id (int), event_type (str), timestamp (str, ISO 8601). Выходной контракт: DataFrame df_clean. Колонки: user_id (int), is_purchase (bool), event_date (datetime64). Ограничения:

  • Использовать только векторизованные операции Pandas, никаких iterrows.
  • is_purchase равно True, если event_type == 'buy'. Действие: Напиши pytest-тест для этого контракта, затем реализуй функцию clean_events(df_raw). Запусти тесты и исправь ошибки, если они будут.

Пространственные трансформации: перестройка формы данных

Самые сложные задачи для генерации — это не математические вычисления, а изменение размерности и формы датасета (Reshaping). Часто данные приходят в «широком» формате (Wide format), удобном для людей, а для машинного обучения или агрегации требуется «длинный» формат (Long format).

Например, у вас есть таблица, где строки — это пользователи, а колонки — месяцы (Jan_revenue, Feb_revenue, Mar_revenue). Матрица имеет размерность M×NM \times N. Для ML-модели это неудобно: время должно быть признаком (фичей), а не названием колонки.

Для этой задачи в Pandas используется операция melt (плавление). Она берет указанные колонки и «схлопывает» их в две новые: одна для названий бывших колонок (переменная), другая для их значений.

При Vibecoding таких пространственных трансформаций LLM часто ошибается с параметрами id_vars (колонки, которые остаются на месте) и value_vars (колонки, которые схлопываются).

Чтобы Claude Code сгенерировал правильный melt, контракт должен явно описывать изменение размерности:

Трансформация: Перевести DataFrame из широкого формата в длинный. id_vars: оставить user_id как якорь. value_vars: схлопнуть все колонки, заканчивающиеся на _revenue. Новые колонки: назвать колонку с месяцами month, колонку со значениями revenue_usd.

Имея такие четкие инструкции, Claude Code напишет идеальный вызов pd.melt(df, id_vars=['user_id'], var_name='month', value_name='revenue_usd') с первого раза, а предварительно написанный тест гарантирует, что количество строк в новой таблице логично увеличилось, а колонок — уменьшилось.

Делегируя написание синтаксиса AI-агенту, вы оставляете за собой проектирование архитектуры данных. Вы строите конвейер, где каждый шаг — это изолированная, тестируемая и строго типизированная (в рамках Pandas) трансформация.

Практика: Конвейер предобработки данных и контроль качества через AI-агентов

Практика: Конвейер предобработки данных и контроль качества через AI-агентов

В бэкенд-разработке на Go мы привыкли, что если код скомпилировался, а unit-тесты прошли, то контракт соблюден. В мире данных (Data Science) это правило не работает. Ваш идеально написанный и протестированный векторизованный код на Pandas может успешно отработать, но выдать на выходе мусор, потому что на вход пришел мусор. Данные мутируют быстрее, чем код.

Мы уже разобрали, как использовать TDDV (Test-Driven Data Vibecoding) для создания отдельных функций трансформации. Теперь соберем из этих функций отказоустойчивый конвейер (pipeline) с помощью автономного агента Claude Code, добавив в него то, чего так не хватает Python по умолчанию — строгий контроль контрактов в рантайме.

Архитектура конвейера: от скрипта к DAG

Senior-подход к обработке данных заключается в отказе от монолитных Jupyter-ноутбуков или длинных скриптов в пользу направленного ациклического графа (DAG). Каждый узел графа — это изолированная функция трансформации со строгими входами и выходами.

Представим задачу: нам нужно подготовить датасет для предсказания оттока пользователей (Churn). Сырые данные лежат в базе, а на выходе нужна матрица признаков (Features).

Разобьем задачу на три узла:

  1. Extract: Загрузка сырых логов сессий и профилей пользователей.
  2. Clean & Validate: Очистка от дубликатов, обработка пропусков (вспоминаем проблему неявного приведения к float64 из-за NaN) и проверка типов.
  3. Feature Engineering: Векторизованное создание новых колонок (например, расчет дней с последней активности).

Чтобы делегировать написание этого графа Claude Code, мы должны описать не шаги реализации, а интерфейсы узлов. В Python роль интерфейса для данных выполняет схема DataFrame.

Рантайм-валидация: защита от "отравленных" данных

В Go мы используем структуры (struct) для жесткой фиксации формы данных. В Pandas структура DataFrame динамична. Если в колонке age внезапно появится строка "двадцать", Pandas не упадет при чтении — он просто изменит тип всей колонки на object, и ваш конвейер тихо сломается на этапе математических вычислений.

Чтобы этого избежать, применяется рантайм-валидация данных. Это слой проверок, который запускается во время выполнения конвейера. Стандартом де-факто для этого в экосистеме Pandas является библиотека pandera.

Она позволяет задать схему, очень похожую на определение структуры в Go:

import pandera as pa
from pandera.typing import DataFrame, Series

class CleanUserSchema(pa.DataFrameModel):
    user_id: Series[int] = pa.Field(ge=1)
    age: Series[float] = pa.Field(ge=18, le=120, nullable=True) # nullable разрешает NaN
    status: Series[str] = pa.Field(isin=["active", "banned"])

Когда мы декорируем функцию трансформации этой схемой, pandera автоматически проверит данные на входе или выходе. Если данные нарушают контракт (например, значение в колонке age меньше 18 или статус неизвестен), конвейер выбросит ошибку до того, как "отравленные" данные попадут в ML-модель.

Тесты кода (pytest) проверяют, правильно ли ваша функция обрабатывает ожидаемые данные. Рантайм-валидация (pandera) проверяет, соответствуют ли реальные данные вашим ожиданиям. Для надежного AI-продукта нужны оба механизма.

Чекпоинтинг: идемпотентность и управление состоянием

Если конвейер обрабатывает миллионы строк и падает на шаге 3 (Feature Engineering) из-за ошибки валидации, начинать весь процесс с шага 1 (выгрузка из БД) — непозволительная трата времени и ресурсов.

Нам нужен чекпоинтинг — сохранение промежуточного состояния (DataFrame) после каждого успешного узла DAG. Это делает конвейер идемпотентным: при перезапуске он прочитает сохраненный результат шага 2 и сразу перейдет к шагу 3.

Здесь возникает критический вопрос выбора формата. Разработчики часто по инерции используют CSV (df.to_csv()). Это фатальная ошибка для конвейеров данных. CSV — текстовый формат, он не хранит метаданные о типах. При чтении CSV Pandas попытается угадать типы заново (Type Inference), что гарантированно разрушит ваши контракты (например, ID в виде строки "00123" превратится в число 123).

Правильный выбор — Parquet (df.to_parquet()). Это бинарный колоночный формат, который:

  1. Сохраняет строгую типизацию данных (включая разницу между int32 и int64).
  2. Сохраняет структуру индексов Pandas.
  3. Работает в 10-50 раз быстрее CSV за счет сжатия на уровне колонок.

Оркестрация Claude Code: собираем всё вместе

Теперь у нас есть архитектура, понимание валидации и механизма сохранения состояния. Пора делегировать рутину Claude Code.

Вместо того чтобы писать код руками, мы создаем файл pipeline_spec.md, который послужит системным контрактом для агента:

# Спецификация конвейера данных (Churn Pipeline)

## Глобальные правила
1. Используем TDDV: сначала тесты (pytest), затем реализация.
2. Все функции трансформации должны быть чистыми (без side-effects).
3. Используем `pandera` для рантайм-валидации выходов каждого шага.
4. После каждого шага сохраняем результат в формате `.parquet` в папку `/data/interim/`.

## Узлы DAG
### Узел 1: extract_users
- Вход: SQL-запрос к БД (используем mock для тестов).
- Выход: DataFrame. Колонки: `user_id` (int), `raw_age` (str), `last_login` (datetime).

### Узел 2: clean_users
- Вход: DataFrame из Узла 1.
- Логика: Конвертировать `raw_age` в число. Если ошибка — ставить NaN.
- Выходная схема Pandera: `user_id` (int), `age` (float, nullable=True).

Запускаем терминал и передаем управление агенту:

claude "Прочитай pipeline_spec.md. Инициализируй проект, создай схемы Pandera, напиши тесты для узла clean_users и реализуй логику до зеленого билда."

Агентный цикл (Agentic Loop) Claude Code начнет работу:

  1. Он прочитает спецификацию.
  2. Создаст файл schemas.py с классами pandera.
  3. Создаст test_pipeline.py с фиктивными данными (включая краевые случаи, например raw_age = "unknown").
  4. Запустит pytest (получит ошибку, так как кода еще нет).
  5. Напишет векторизованную реализацию clean_users в pipeline.py, используя pd.to_numeric(..., errors='coerce') для безопасного парсинга.
  6. Запустит тесты снова. Если они пройдут — задача выполнена.

Вы, как Senior-разработчик, в этот момент выступаете в роли ревьюера. Вы не тратите время на поиск нужного флага в документации Pandas, вы проверяете, правильно ли агент выстроил поток данных и не забыл ли он про чекпоинты в Parquet.

Именно так выглядит современная AI-разработка: вы проектируете графы состояний и контракты, а ИИ заполняет их исполняемым кодом. Подготовленный таким образом чистый и валидированный датасет — это фундамент, на котором в следующих главах мы начнем строить настоящие математические модели и нейросети.