Перейти к основному содержимому

РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ Data Scientist / ML Engineer RaccoonSoft - Junior 90+ тыс

· 85 мин. чтения

Сегодня мы разберем спокойное и доброжелательное техническое собеседование на джун-ML/ Python позицию, в котором интервьюер аккуратно прощупывает базу кандидата: от Python и ООП до базовых алгоритмов, SQL, NumPy/Pandas и основ машинного обучения. В ходе диалога видно, что кандидат ориентируется в ключевых инструментах и учебных задачах, но местами дает поверхностные или неточные ответы, что подчёркивает формат встречи как «growth potential» интервью, а не жёсткий отбор.

Вопрос 1. Знаком ли ты с тестированием кода и подходом разработки через тестирование (TDD)?

Таймкод: 00:01:36

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

Правильный ответ:

В промышленной разработке на Go тестирование — обязательная часть процесса, а не опция. Важно понимать несколько уровней: unit-тесты, интеграционные тесты, end-to-end тесты, property-based и нагрузочное тестирование, а также подходы вроде TDD.

Основные подходы к тестированию:

  1. Unit-тесты:

    • Тестируют отдельные функции/методы в изоляции.
    • Не ходят во внешние сервисы (БД, сети, файловая система); все внешнее выносится в интерфейсы и подменяется моками/стабами.
    • Быстрые, запускаются постоянно (locally, pre-commit, CI).
  2. Интеграционные тесты:

    • Проверяют связку компонентов: сервис + БД, сервис + очередь, HTTP API и т.д.
    • Используют реальные зависимости или их максимально приближенные аналоги (docker-compose в CI, testcontainers).
    • Медленнее, запускаются реже, но критичны для уверенности в реальной работоспособности.
  3. End-to-end (E2E) и системные тесты:

    • Проверяют полный сценарий работы системы “как пользователь”.
    • Часто пишутся поверх API/UI.
    • Дороже и медленнее, но ловят проблемы, которые не видны на unit-уровне.
  4. Property-based тестирование:

    • Вместо фиксированных кейсов формулируются свойства, которые должны выполняться для множества входных данных.
    • Используется для сложных алгоритмов, парсеров, трансформаций данных.
    • В Go — библиотеки вроде github.com/leanovate/gopter или testing/quick.
  5. Нагрузочное и перформанс-тестирование:

    • Проверяют поведение под нагрузкой, latency, throughput, конкуренцию.
    • Для Go — go test -bench, pprof, внешние инструменты (k6, vegeta, wrk).

Подход TDD (Test-Driven Development):

Суть не в тестах как таковых, а в цикле:

  1. Написать падающий тест (red):

    • Описать желаемое поведение функции/компонента.
    • Тест должен явно проваливаться, потому что реализации еще нет.
  2. Реализовать минимальный код (green):

    • Написать простейшую реализацию, чтобы тест прошел.
    • Не думать сразу о идеальной архитектуре, только корректность.
  3. Рефакторинг (refactor):

    • Упростить, обобщить, сделать код чище.
    • Все тесты должны оставаться зелеными.

Что дает TDD:

  • Дизайн через использование: API проектируется “глазами потребителя”.
  • Меньше скрытых зависимостей и побочных эффектов.
  • Выше покрытие тестами и предсказуемость изменений.
  • Легче рефакторить — тесты являются “страховкой”.

Пример unit-теста на Go:

// file: sum.go
package calc

func Sum(a, b int) int {
return a + b
}
// file: sum_test.go
package calc

import "testing"

func TestSum(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"zero", 0, 0, 0},
{"negative", -1, -2, -3},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if res := Sum(tt.a, tt.b); res != tt.expected {
t.Fatalf("expected %d, got %d", tt.expected, res)
}
})
}
}

Пример интеграционного теста с БД (SQL):

// Псевдо-пример, упрощенно
func TestUserRepository_CreateAndGet(t *testing.T) {
db, cleanup := setupTestDB(t) // поднимаем тестовую БД (docker/testcontainers)
defer cleanup()

repo := NewUserRepository(db)

ctx := context.Background()
u := User{Name: "John", Email: "john@example.com"}

if err := repo.Create(ctx, &u); err != nil {
t.Fatalf("create user: %v", err)
}

got, err := repo.GetByEmail(ctx, "john@example.com")
if err != nil {
t.Fatalf("get user: %v", err)
}

if got.Email != u.Email {
t.Fatalf("expected email %s, got %s", u.Email, got.Email)
}
}

Соответствующий SQL (миграция для тестовой БД):

CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);

Практические моменты в Go:

  • Использовать стандартный пакет testing как основу.
  • Для моков — интерфейсы + самописные стабы или библиотеки (gomock, testify/mock).
  • Группировка кейсов через t.Run, использование t.Helper() для вспомогательных функций.
  • Запуск тестов: go test ./....
  • Интеграция в CI/CD: тесты должны запускаться автоматически на каждом коммите/PR.
  • Покрытие: go test ./... -cover -coverprofile=coverage.out.

Важно не просто “знать про тесты”, а системно применять:

  • Писать тесты до или во время реализации критичных компонентов.
  • Декомпозировать функционал так, чтобы его можно было протестировать.
  • Минимизировать глобальное состояние и жесткие зависимости.

Такое отношение к тестированию делает код надежным, сопровождаемым и предсказуемым при изменениях.

Вопрос 2. Что ты знаешь о шаблонах проектирования и применяешь ли их на практике?

Таймкод: 00:02:25

Ответ собеседника: неправильный. Кандидат ассоциирует шаблоны проектирования в основном с ООП и общими принципами, признаёт, что почти не изучал их и не применяет осознанно, ссылаясь на фокус на ML, а не на разработку.

Правильный ответ:

Шаблоны проектирования — это не про «ООП из книжки», а про типовые архитектурные решения, которые помогают делать код удобнее для сопровождения, расширения и тестирования. В Go их важно понимать не формально по “Gang of Four”, а с учетом особенностей языка: интерфейсы, композиция, функции как значения, простые структуры, конкуренция через goroutine и каналы.

Ключевые идеи использования шаблонов в Go:

  • Не навешивать паттерны ради паттернов.
  • Использовать их как язык общения в команде: “тут у нас Strategy”, “здесь Builder”, “это Decorator”.
  • Подбирать решения под стиль Go: простота, явность, минимализм, композиция вместо сложных иерархий.

Ниже — обзор основных шаблонов и то, как они выглядят в Go с примерами.

Порождающие шаблоны:

  1. Factory (Simple Factory / Factory Method)

    • Когда:
      • Нужно создавать объекты с инкапсулированной логикой (конфигурация, валидация, выбор конкретной реализации).
    • В Go:
      • Часто просто функция-конструктор, возвращающая интерфейс.

    Пример:

    type Storage interface {
    Save(ctx context.Context, key string, data []byte) error
    }

    type S3Storage struct { /* ... */ }
    type FileStorage struct { /* ... */ }

    func NewStorage(kind string) (Storage, error) {
    switch kind {
    case "s3":
    return NewS3Storage(/* cfg */), nil
    case "file":
    return NewFileStorage(/* cfg */), nil
    default:
    return nil, fmt.Errorf("unknown storage type: %s", kind)
    }
    }
  2. Builder

    • Когда:
      • Объект сложный, много опций, не хочется перегружать конструкторы.
    • В Go:
      • Часто реализуется через функциональные опции.

    Пример:

    type Server struct {
    addr string
    timeout time.Duration
    logger Logger
    }

    type Option func(*Server)

    func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
    }

    func WithLogger(l Logger) Option {
    return func(s *Server) { s.logger = l }
    }

    func NewServer(addr string, opts ...Option) *Server {
    s := &Server{
    addr: addr,
    timeout: 5 * time.Second,
    logger: defaultLogger,
    }
    for _, opt := range opts {
    opt(s)
    }
    return s
    }

Структурные шаблоны:

  1. Adapter

    • Когда:
      • Нужно подружить несовместимые интерфейсы (например, внешняя библиотека и ваш код).
    • В Go:
      • Реализуется тонкой оберткой, удовлетворяющей нужному интерфейсу.

    Пример:

    type Logger interface {
    Info(msg string)
    }

    type ZapAdapter struct {
    l *zap.SugaredLogger
    }

    func (z ZapAdapter) Info(msg string) {
    z.l.Infow(msg)
    }
  2. Decorator

    • Когда:
      • Нужно добавить поведение (логирование, метрики, кэширование, ретраи) без изменения основной реализации.
    • В Go:
      • Очень распространен для репозиториев, клиентов, хендлеров.

    Пример:

    type UserRepo interface {
    Get(ctx context.Context, id int64) (*User, error)
    }

    type loggingUserRepo struct {
    next UserRepo
    logger Logger
    }

    func (r loggingUserRepo) Get(ctx context.Context, id int64) (*User, error) {
    start := time.Now()
    user, err := r.next.Get(ctx, id)
    r.logger.Info(fmt.Sprintf("Get user=%d took=%s err=%v", id, time.Since(start), err))
    return user, err
    }

    func WithLogging(repo UserRepo, logger Logger) UserRepo {
    return loggingUserRepo{next: repo, logger: logger}
    }
  3. Facade

    • Когда:
      • Есть сложная подсистема, и вы хотите дать простой API.
    • В Go:
      • Типичный паттерн для модулей: “внешний” пакет с простыми функциями скрывает сложную инфраструктуру.

Поведенческие шаблоны:

  1. Strategy

    • Когда:
      • Нужно взаимозаменяемо использовать разные алгоритмы с одним интерфейсом.
    • В Go:
      • Или интерфейсы, или функции как значения.

    Пример:

    type SortStrategy func([]int)

    func SortAscending(nums []int) { sort.Ints(nums) }
    func SortDescending(nums []int) { sort.Sort(sort.Reverse(sort.IntSlice(nums))) }

    func Process(nums []int, strategy SortStrategy) {
    strategy(nums)
    // дальше работа с отсортированными данными
    }
  2. Observer (Event-driven)

    • Когда:
      • Надо уведомлять множество подписчиков об изменении состояния.
    • В Go:
      • Часто реализуется через каналы или callback-и.

    Пример (упрощенно):

    type Event struct {
    Type string
    Data any
    }

    type Bus struct {
    subs []chan Event
    }

    func (b *Bus) Subscribe() <-chan Event {
    ch := make(chan Event, 10)
    b.subs = append(b.subs, ch)
    return ch
    }

    func (b *Bus) Publish(e Event) {
    for _, ch := range b.subs {
    select {
    case ch <- e:
    default:
    }
    }
    }
  3. Chain of Responsibility

    • Когда:
      • Надо пропустить запрос через цепочку обработчиков (валидация, авторизация, логирование).
    • В Go:
      • Очень естественно смотрится на middleware в HTTP.

    Пример:

    type Middleware func(http.Handler) http.Handler

    func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
    h = mws[i](h)
    }
    return h
    }

Шаблоны и SQL/хранилища:

  • Repository:

    • Инкапсулирует доступ к данным, не размывая SQL по коду.
    • Облегчает тестирование (моки интерфейсов).
    type User struct {
    ID int64
    Name string
    Email string
    }

    type UserRepository interface {
    Create(ctx context.Context, u *User) error
    GetByID(ctx context.Context, id int64) (*User, error)
    }

    type userRepo struct {
    db *sql.DB
    }

    func (r *userRepo) Create(ctx context.Context, u *User) error {
    query := `INSERT INTO users(name, email) VALUES($1, $2) RETURNING id`
    return r.db.QueryRowContext(ctx, query, u.Name, u.Email).Scan(&u.ID)
    }

    func (r *userRepo) GetByID(ctx context.Context, id int64) (*User, error) {
    var u User
    query := `SELECT id, name, email FROM users WHERE id = $1`
    err := r.db.QueryRowContext(ctx, id).Scan(&u.ID, &u.Name, &u.Email)
    if err != nil {
    return nil, err
    }
    return &u, nil
    }

    SQL-схема:

    CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
    );

Ключевые практические моменты:

  • Шаблоны — это инструмент для:
    • осознанного разделения ответственностей;
    • уменьшения связности (coupling) и повышения тестируемости;
    • гибкости к изменениям (новые алгоритмы, поставщики, хранилища).
  • Важно:
    • Понимать intent каждого паттерна: какую проблему он решает.
    • Уметь узнавать паттерны в коде и объяснить, почему они применены.
    • Сдержанно использовать: если решение можно выразить простым кодом без паттерна — так и делаем.

Осознанное владение шаблонами проектирования на практике показывает умение мыслить архитектурно, а не просто писать “чтобы работало”.

Вопрос 3. Объясни, как ты понимаешь принципы SOLID с примерами.

Таймкод: 00:03:21

Ответ собеседника: неполный. Кандидат знает, что SOLID содержит пять принципов, частично пояснил принцип единой ответственности и открытости/закрытости, но не смог внятно объяснить принцип подстановки Лисков и остальные принципы.

Правильный ответ:

SOLID — это набор принципов проектирования, цель которых сделать код:

  • устойчивым к изменениям,
  • простым для расширения,
  • удобным для тестирования,
  • понятным для команды.

Эти принципы применимы не только к классическому ООП, но и к Go, где мы работаем с композициями, интерфейсами, функциями и пакетами.

Разберем каждый принцип с акцентом на практику и примеры на Go.

  1. Single Responsibility Principle (SRP) — Принцип единой ответственности

Формулировка: Компонент (модуль, структура, функция, пакет) должен иметь только одну причину для изменения.

Суть:

  • Не смешивать в одном месте разные области ответственности:
    • бизнес-логика,
    • логирование,
    • работа с БД,
    • HTTP,
    • валидация,
    • конфигурация и т.д.
  • Это уменьшает связность и упрощает тестирование.

Пример (нарушение SRP):

type UserService struct {
db *sql.DB
logger *log.Logger
}

func (s *UserService) Register(ctx context.Context, name, email string) error {
// валидация
if name == "" || email == "" {
return errors.New("invalid input")
}

// логирование
s.logger.Printf("register user %s", email)

// SQL (доступ к БД)
_, err := s.db.ExecContext(ctx,
`INSERT INTO users(name, email) VALUES($1, $2)`, name, email)
return err
}

Здесь один компонент отвечает за:

  • бизнес-правила,
  • логирование,
  • доступ к БД.

Лучше разделить: выносим хранилище и логгер в зависимости:

type UserRepository interface {
Create(ctx context.Context, name, email string) error
}

type UserService struct {
repo UserRepository
logger Logger
}

func (s *UserService) Register(ctx context.Context, name, email string) error {
if name == "" || email == "" {
return errors.New("invalid input")
}

s.logger.Info("register user " + email)
return s.repo.Create(ctx, name, email)
}

Теперь:

  • UserService отвечает за бизнес-логику регистрации.
  • Repository — за работу с БД.
  • Логгер — за логирование.
  • Все части легче тестировать и менять независимо.
  1. Open/Closed Principle (OCP) — Принцип открытости/закрытости

Формулировка: Модуль должен быть открыт для расширения, но закрыт для изменения.

Суть:

  • Чтобы добавить новое поведение, мы должны по возможности не менять существующий код (особенно стабилизированный), а расширять его через:
    • интерфейсы,
    • композицию,
    • конфигурацию,
    • плагины/стратегии.

Пример: Допустим, у нас есть отправка уведомлений.

Анти-подход:

func SendNotification(kind, to, msg string) error {
switch kind {
case "email":
// отправляем email
case "sms":
// отправляем sms
default:
return fmt.Errorf("unsupported kind: %s", kind)
}
return nil
}

Каждый новый тип уведомления — правка switch, пересборка, риск поломок.

Лучше через интерфейс:

type Notifier interface {
Notify(to, msg string) error
}

type EmailNotifier struct { /* ... */ }
func (n EmailNotifier) Notify(to, msg string) error { /* ... */ return nil }

type SMSNotifier struct { /* ... */ }
func (n SMSNotifier) Notify(to, msg string) error { /* ... */ return nil }

func SendWelcome(n Notifier, to string) error {
return n.Notify(to, "Welcome!")
}

Теперь:

  • Чтобы добавить SlackNotifier, мы не меняем SendWelcome, а просто реализуем новый Notifier.
  • Это и есть OCP в духе Go.
  1. Liskov Substitution Principle (LSP) — Принцип подстановки Лисков

Формулировка (адаптация для Go): Любая реализация интерфейса должна быть взаимозаменяемой без нарушения ожидаемого поведения клиента.

Суть:

  • Если у нас есть интерфейс, то любая его реализация:
    • не должна ломать инварианты,
    • не должна требовать дополнительных предусловий,
    • не должна возвращать неожиданные нарушения контракта.
  • В Go это критично, потому что интерфейсы неявные: важно документировать поведение, а не только сигнатуры.

Пример нарушения:

type Cache interface {
Get(key string) (string, error)
}

type MapCache struct {
m map[string]string
}

func (c MapCache) Get(key string) (string, error) {
v, ok := c.m[key]
if !ok {
return "", errors.New("not found")
}
return v, nil
}

type PanicCache struct{}

func (PanicCache) Get(key string) (string, error) {
panic("key not found") // жестко
}

Оба типа реализуют интерфейс Cache, но PanicCache нарушает ожидаемое поведение:

  • Клиент ожидает error при отсутствии ключа,
  • Вместо этого получит панику.

LSP в Go:

  • Интерфейсы должны описывать не только сигнатуру, но и семантику (контракт).
  • Реализация не должна усиливать предусловия и ломать этот контракт.
  1. Interface Segregation Principle (ISP) — Принцип разделения интерфейсов

Формулировка: Клиенты не должны зависеть от интерфейсов, которые они не используют.

Суть:

  • Лучше несколько маленьких интерфейсов, чем один “толстый”.
  • Это идеально ложится на философию Go.

Плохой пример:

type Repository interface {
Create(ctx context.Context, u *User) error
Get(ctx context.Context, id int64) (*User, error)
Update(ctx context.Context, u *User) error
Delete(ctx context.Context, id int64) error
}

Если где-то нужен только Get, приходится тянуть весь интерфейс.

Лучше так:

type UserCreator interface {
Create(ctx context.Context, u *User) error
}

type UserReader interface {
Get(ctx context.Context, id int64) (*User, error)
}

type UserUpdater interface {
Update(ctx context.Context, u *User) error
}

type UserDeleter interface {
Delete(ctx context.Context, id int64) error
}

И собирать композиции там, где нужно.

Плюсы:

  • Меньше фиктивных зависимостей.
  • Проще мокать конкретные сценарии в тестах.
  • Код чище и стабильнее при изменениях.
  1. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей

Формулировка:

  • Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций.

В Go:

  • Используем интерфейсы и конструкторы, чтобы не "вшивать" конкретные реализации внутрь бизнес-логики.
  • Верхний слой (domain / use-cases) определяет интерфейсы.
  • Инфраструктура (БД, HTTP-клиенты, очереди) эти интерфейсы реализует.

Пример:

// уровень домена
type Mailer interface {
SendResetPassword(email, token string) error
}

type PasswordService struct {
mailer Mailer
}

func (s *PasswordService) SendResetLink(email string) error {
token := generateToken()
return s.mailer.SendResetPassword(email, token)
}
// инфраструктура
type SMTPMailer struct {
// конфиг, клиент
}

func (m *SMTPMailer) SendResetPassword(email, token string) error {
// отправка через SMTP
return nil
}

Связка:

func NewApp() *PasswordService {
smtpMailer := &SMTPMailer{/*...*/}
return &PasswordService{mailer: smtpMailer}
}

DIP на практике:

  • Легко подменять зависимости в тестах (моки вместо реальных SMTP/БД).
  • Легко менять реализацию без переписывания бизнес-логики (SMTP → SendGrid → Kafka и т.д.).
  • Архитектура становится устойчивее к изменениям инфраструктуры.

Кратко о применении SOLID в Go:

  • Не воспринимать SOLID как теорию из OOP-учебников.
  • В Go это выражается через:
    • маленькие, точные интерфейсы;
    • явные зависимости через конструкторы;
    • композицию вместо наследования;
    • разделение зон ответственности по пакетам и слоям;
    • понятные контракты для интерфейсов (LSP).
  • Цель — не “соблюсти букву SOLID”, а сделать код:
    • легко тестируемым,
    • простым для изменения,
    • предсказуемым под нагрузкой реального проекта.

Вопрос 4. В чём разница между оператором "is" и оператором "==" в Python?

Таймкод: 00:05:15

Ответ собеседника: правильный. Оператор is проверяет, ссылаются ли переменные на один и тот же объект в памяти, тогда как == сравнивает равенство значений.

Правильный ответ:

В Python:

  • ==:

    • Логическое сравнение значений.
    • Вызывает метод __eq__ объекта (если он определён).
    • Два разных объекта могут быть равны по значению, но не быть одним и тем же объектом.
  • is:

    • Проверка идентичности объекта.
    • Возвращает True, если обе ссылки указывают на один и тот же объект (один и тот же адрес в памяти).
    • Используется для:
      • сравнения с синглтонами (None, True, False, иногда Ellipsis);
      • проверок, что это именно тот же объект, а не только равный по значению.

Примеры:

a = [1, 2, 3]
b = [1, 2, 3]

print(a == b) # True - значения совпадают
print(a is b) # False - разные объекты в памяти

c = a
print(c is a) # True - одна и та же ссылка

x = None
print(x is None) # Правильно
print(x == None) # Работает, но стиль хуже

Особенность с интернированием (например, маленькие целые числа, строки): Иногда is даёт True «случайно» из-за оптимизаций интерпретатора, но полагаться на это для сравнения значений нельзя:

a = 256
b = 256
print(a is b) # Может быть True из-за интернирования

a = 1000
b = 1000
print(a is b) # Зависит от реализации, использовать так нельзя

Практический вывод:

  • Для сравнения значений использовать ==.
  • Для проверки на None и идентичность объекта использовать is.
  • Не использовать is для сравнения обычных значений (строки, числа, коллекции) — это логическая ошибка.

Вопрос 5. В чём разница между методами init и new в Python?

Таймкод: 00:05:37

Ответ собеседника: правильный. new создаёт новый объект, init инициализирует уже созданный; порядок вызова: сначала new, затем init.

Правильный ответ:

В Python создание объекта — это двухшаговый процесс: сначала объект создаётся, затем инициализируется. За это отвечают два разных механизма:

  1. __new__(cls, *args, **kwargs) — создание объекта
  2. __init__(self, *args, **kwargs) — инициализация объекта

Основные различия:

  • __new__:

    • Статически привязанный (по сути, это метод класса).
    • На вход получает класс (cls) и аргументы конструктора.
    • Обязан вернуть новый объект (обычно через super().__new__(cls)).
    • Вызывается первым при создании экземпляра.
    • Используется, когда нужно контролировать процесс создания:
      • неизменяемые типы (int, str, tuple);
      • синглтоны, пулы объектов;
      • фабричное поведение: вернуть экземпляр другого класса или кэшированный объект.
  • __init__:

    • Обычный метод экземпляра.
    • На вход получает уже созданный объект (self) и те же аргументы (как правило).
    • Ничего не должен возвращать (возврат игнорируется).
    • Отвечает за установку состояния объекта: поля, проверки и т.п.
    • Вызывается только если __new__ вернул объект данного класса. Если __new__ вернул объект другого типа — __init__ может не вызываться.

Простой пример стандартного поведения:

class User:
def __new__(cls, *args, **kwargs):
# Создание объекта (обычно так)
instance = super().__new__(cls)
return instance

def __init__(self, name: str):
# Инициализация уже созданного объекта
self.name = name

u = User("Alice")
print(u.name) # Alice

Пример, где нужен __new__: контроль создания (например, синглтон):

class Singleton:
_instance = None

def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self, value):
# Будет вызываться при каждом создании, важно учитывать
self.value = value

a = Singleton(1)
b = Singleton(2)

print(a is b) # True — один и тот же объект
print(a.value) # 2
print(b.value) # 2

Пример с неизменяемым типом:

class MyInt(int):
def __new__(cls, value):
# Можем модифицировать создаваемое значение
return super().__new__(cls, value * 2)

def __init__(self, value):
# Для неизменяемых типов состояние задаётся в __new__,
# __init__ обычно не нужен или пустой.
pass

x = MyInt(5)
print(x) # 10

Ключевые практические выводы:

  • В 99% случаев достаточно переопределять __init__.
  • __new__ переопределяется, когда:
    • работаем с неизменяемыми типами;
    • хотим управлять кэшированием/пулами/синглтонами;
    • возможно, возвращаем объект другого класса.
  • Важно помнить: если __new__ не возвращает экземпляр текущего класса, __init__ может не быть вызван.

Вопрос 6. Что такое механизм slots в классах Python и для чего он используется?

Таймкод: 00:06:13

Ответ собеседника: неправильный. Кандидат не знал про slots; интервьюер пояснил, что это способ зафиксировать набор атрибутов.

Правильный ответ:

Механизм __slots__ в Python позволяет:

  • Явно задать фиксированный набор атрибутов экземпляра.
  • Убрать динамический словарь атрибутов (__dict__) и, как следствие:
    • уменьшить потребление памяти,
    • немного ускорить доступ к атрибутам,
    • запретить создание произвольных новых полей "на лету".

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

Если определить __slots__, интерпретатор:

  • Создаёт дескрипторы для указанных имён атрибутов.
  • Не создаёт __dict__ (если явно не указано).
  • Не позволяет присваивать неописанные в __slots__ атрибуты (возникает AttributeError).

Базовый пример:

class User:
__slots__ = ("id", "name", "email")

def __init__(self, user_id: int, name: str, email: str):
self.id = user_id
self.name = name
self.email = email

u = User(1, "Alice", "a@example.com")
u.name = "Bob" # ОК
u.age = 30 # AttributeError: 'User' object has no attribute 'age'

Что это даёт:

  • Для больших коллекций объектов экономия памяти может быть значительной.
  • Ускоряется доступ к атрибутам (нет словарного поиска, есть фиксированные слоты).

Важно понимать детали и ограничения:

  1. Наследование:

    • Если подкласс не определяет __slots__, у него снова появится __dict__, и ограничения исчезнут.
    • Если определяет, нужно учитывать слоты родителя.
    class Base:
    __slots__ = ("id",)

    class Child(Base):
    __slots__ = ("name",)

    c = Child()
    c.id = 1
    c.name = "Alice"
    # c.__dict__ не существует
  2. __dict__ и __weakref__:

    • Если нужно сохранить возможность динамически добавлять поля, можно явно добавить "__dict__" в __slots__.
    • Для weakref-ов — "__weakref__".
    class Flexible:
    __slots__ = ("id", "__dict__")

    f = Flexible()
    f.id = 1
    f.extra = "ok" # теперь работает, т.к. есть __dict__
  3. Когда использовать:

    • Классы, от которых создаются миллионы объектов (например, структуры данных, модели для in-memory обработок).
    • Performance- и memory-sensitive участки кода.
    • Если модель данных фиксирована и не предполагает динамических полей.
  4. Когда не стоит:

    • В прототипах и динамичных моделях, где добавляются поля на лету.
    • Там, где важна гибкость больше, чем экономия памяти.
    • В сложных иерархиях, где __slots__ делает код менее очевидным.

Кратко:

  • __slots__ — инструмент оптимизации и ограничения структуры объектов.
  • Он фиксирует список допустимых атрибутов, уменьшает память и ускоряет доступ.
  • Важно осознанно применять его там, где есть реальная нагрузка, а не “по привычке”.

Вопрос 7. Что делают функции map, filter и reduce и как ими правильно пользоваться?

Таймкод: 00:06:38

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

Правильный ответ:

Функции map, filter и reduce — это классические инструменты функционального стиля программирования для работы с коллекциями. Они помогают выразить операции над данными декларативно: что мы хотим сделать, а не как по шагам.

В общем виде:

  • map — трансформация элементов.
  • filter — отбор элементов по условию.
  • reduce — свёртка/агрегация всех элементов к одному значению.

Хотя в Python часто предпочтительнее использовать списковые включения и генераторы, понимание этих функций важно — они встречаются и в Python-коде, и в других языках / платформах (включая концептуальные аналоги в Go).

  1. map

Назначение:

  • Применяет функцию к каждому элементу последовательности, возвращает новый итерируемый объект с результатами.

Сигнатура:

  • map(func, iterable, *iterables)

Пример:

nums = [1, 2, 3, 4]

# Удвоение каждого элемента:
res = list(map(lambda x: x * 2, nums))
# res = [2, 4, 6, 8]

С несколькими итерируемыми:

a = [1, 2, 3]
b = [10, 20, 30]

res = list(map(lambda x, y: x + y, a, b))
# res = [11, 22, 33]

Идиоматичный Python:

  • В простых случаях лучше списковые включения:
res = [x * 2 for x in nums]
  1. filter

Назначение:

  • Отбирает элементы, для которых функция-предикат возвращает True.

Сигнатура:

  • filter(func, iterable)

Пример:

nums = [1, 2, 3, 4, 5, 6]

# Оставить только чётные:
res = list(filter(lambda x: x % 2 == 0, nums))
# res = [2, 4, 6]

Идиоматичный Python:

  • Через генераторы:
res = [x for x in nums if x % 2 == 0]
  1. reduce

Назначение:

  • Пошагово "сворачивает" последовательность в одно значение, применяя бинарную функцию.

Подключение:

  • В Python 3 находится в functools.

Сигнатура:

  • functools.reduce(func, iterable[, initializer])

Пример — сумма элементов:

from functools import reduce

nums = [1, 2, 3, 4]

total = reduce(lambda acc, x: acc + x, nums, 0)
# total = 10

Пример — конкатенация строк:

words = ["Go", "is", "fast"]

sentence = reduce(lambda acc, w: acc + " " + w, words)
# "Go is fast"

Типичный паттерн работы:

  • acc (accumulator) хранит промежуточный результат.
  • Для каждого элемента x вычисляем новый acc.

Во многих случаях reduce в Python менее читаем, чем явный цикл:

total = 0
for x in nums:
total += x
  1. Практические моменты и подводные камни
  • Читаемость:

    • В Python сообщества часто предпочитает list/generator comprehensions вместо map/filter с lambda, если это делает код понятнее.
  • map/filter без lambda:

    • Можно передавать существующие функции:
res = list(map(str.upper, ["a", "b", "c"]))
# ["A", "B", "C"]
res = list(filter(str.isdigit, ["1", "x", "3"]))
# ["1", "3"]
  • Ленивая семантика:
    • map и filter возвращают итераторы в Python 3, вычисляются по мере обхода.
it = map(lambda x: x * 2, range(10**9))
# вычисление произойдёт только при итерировании
  1. Аналогии в Go (важно для мышления)

В Go нет встроенных map/filter/reduce как функций высшего порядка для слайсов, но подходы те же:

  • map → цикл, создающий новый слайс.
  • filter → цикл с условием.
  • reduce → цикл с аккумулятором.

Пример map/filter/reduce на Go (явно):

// map: умножить каждый элемент на 2
func MapMul2(nums []int) []int {
res := make([]int, len(nums))
for i, v := range nums {
res[i] = v * 2
}
return res
}

// filter: оставить только чётные
func FilterEven(nums []int) []int {
res := make([]int, 0, len(nums))
for _, v := range nums {
if v%2 == 0 {
res = append(res, v)
}
}
return res
}

// reduce: сумма
func ReduceSum(nums []int) int {
sum := 0
for _, v := range nums {
sum += v
}
return sum
}

Ключевой вывод:

  • map, filter, reduce — базовые абстракции для обработки коллекций.
  • Нужно уверенно:
    • знать их сигнатуры;
    • понимать семантику;
    • уметь показать пару простых примеров;
    • осознавать, когда они уместны, а когда лучше явный цикл ради читаемости.

Вопрос 8. Для чего используется функция id() в Python?

Таймкод: 00:07:24

Ответ собеседника: неполный. Сначала отвечает расплывчато, затем после подсказки соглашается, что функция возвращает адрес (идентификатор) объекта в памяти.

Правильный ответ:

Функция id(obj) в Python возвращает уникальный идентификатор объекта на момент его существования.

Ключевые моменты:

  • id(x):

    • Возвращает целое число, которое:
      • в CPython обычно соответствует адресу объекта в памяти;
      • в других реализациях (PyPy, Jython и т.п.) может быть реализовано иначе, но гарантируется уникальность для "живых" объектов.
    • Уникальность:
      • Пока объект существует, его id уникален среди всех текущих объектов.
      • После удаления объекта (GC) его идентификатор теоретически может быть переиспользован.
  • Основные применения:

    • Отладка:
      • Проверить, ссылаются ли две переменные на один и тот же объект.
      • Проследить, когда создаются/копируются/шарятся объекты.
    • Демонстрация отличий между:
      • is (сравнение идентичности) и == (сравнение значений).
      • мутабельными и иммутабельными объектами при операциях.
  • Важное замечание:

    • id() не следует использовать как бизнес-идентификатор объекта или ключ в системной логике.
    • Это внутренний технический идентификатор времени жизни объекта в рамках конкретного процесса интерпретатора.

Примеры:

a = [1, 2, 3]
b = a
c = [1, 2, 3]

print(id(a), id(b), id(c))
# id(a) == id(b), потому что b ссылается на тот же объект
# id(c) отличается, хотя значения равны

print(a is b) # True
print(a is c) # False
print(a == c) # True

Пример для иллюстрации интернирования:

x = 256
y = 256
print(id(x), id(y)) # часто совпадают в CPython

x = 1000
y = 1000
print(id(x), id(y)) # могут быть разными

Практический вывод:

  • id() полезна для понимания/диагностики поведения ссылок и оптимизаций, но не должна использоваться как часть прикладной логики.

Вопрос 9. Как работает конструкция try-except-else в Python и когда выполняется блок else?

Таймкод: 00:08:07

Ответ собеседника: правильный. Кандидат верно передал суть: блок except выполняется при ошибке в try, блок else — если в блоке try исключения не произошло.

Правильный ответ:

Конструкция try-except-else в Python позволяет явно разделить:

  • код, в котором потенциально могут возникнуть исключения;
  • обработку исключений;
  • код, который должен выполниться только при отсутствии ошибок.

Общий порядок выполнения:

try:
# 1. Код, где может возникнуть исключение
except SomeError:
# 2a. Обработка конкретного(ых) исключения(ий), если они произошли в try
else:
# 2b. Выполняется ТОЛЬКО если:
# - в блоке try НЕ было исключений
# - и ни один except не отрабатывал

Подробно по шагам:

  1. Выполняется блок try.
  2. Если в try произошло исключение:
    • Python ищет подходящий except по типу исключения;
    • если найден — выполняется соответствующий блок except;
    • блок else при этом пропускается.
  3. Если в try исключений не было:
    • блоки except пропускаются;
    • выполняется блок else (если он есть).

Важно:

  • Блок else не ловит исключения внутри себя.
  • Исключения, возникшие в else, идут дальше вверх по стеку, как обычно.

Практический смысл else:

  • Повышение читаемости и точности обработки ошибок.
  • В try оставляем только код, от которого ожидаем исключения.
  • В else пишем действия, которые должны выполняться только в случае успешного завершения try и не относятся к обработке исключений.

Пример:

def read_int_from_file(path):
try:
with open(path, "r") as f:
data = f.read().strip()
value = int(data) # здесь может быть ValueError
except FileNotFoundError:
return None # файл не найден — обрабатываем
except ValueError:
return None # некорректное число — обрабатываем
else:
# Выполняется только если:
# - файл успешно открыт
# - значение успешно прочитано и преобразовано в int
return value

С добавлением finally (для полноты картины):

try:
# код
except SomeError:
# обработка
else:
# если ошибок не было
...
finally:
# выполняется ВСЕГДА (были ошибки или нет)
# обычно освобождение ресурсов
...

Ключевая идея:

  • else — это "код успеха", который запускается только тогда, когда try прошёл без исключений, и не содержит логики обработки ошибок. Это делает контроль потоков и ошибок более явным и чистым.

Вопрос 10. Что такое декоратор в Python и зачем он нужен?

Таймкод: 00:08:42

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

Правильный ответ:

Декоратор в Python — это паттерн и синтаксический сахар для функции (или класса), которая принимает другую функцию/метод/класс и возвращает изменённую или обёрнутую версию, тем самым добавляя дополнительное поведение без изменения исходного кода целевой функции.

Ключевые идеи:

  • Декоратор:
    • принимает функцию (или метод, или класс) как аргумент;
    • возвращает новую функцию (обёртку), которая:
      • вызывает оригинальную,
      • добавляет что-то до/после/вокруг вызова (логирование, метрики, кеширование, проверки и т.д.).
  • Это классический пример "decorator pattern" из проектирования, реализованный средствами функций высшего порядка.
  • Важен для:
    • кросс-срезочной логики (cross-cutting concerns): логирование, аутентификация, ретраи, транзакции;
    • декларативных API во фреймворках (Flask, FastAPI, Django, etc.).

Базовый пример декоратора:

def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper

@log_calls
def add(a, b):
return a + b

add(2, 3)
# Выведет:
# Calling add with args=(2, 3), kwargs={}
# add returned 5

Эквивалент без синтаксического сахара:

def add(a, b):
return a + b

add = log_calls(add) # вручную "задекорировали" функцию

Типичные сценарии применения:

  1. Логирование и трассировка:

    • Добавить логирование входов/выходов, не меняя бизнес-логику.
  2. Авторизация и аутентификация:

    • Веб-фреймворки используют декораторы для проверки прав доступа:
    • например, @login_required.
  3. Ретраи:

    • Повторять вызов функции при временных ошибках (сети, БД, внешние API).
  4. Кеширование:

    • functools.lru_cache — стандартный декоратор для memoization.
    from functools import lru_cache

    @lru_cache(maxsize=1024)
    def fib(n: int) -> int:
    if n < 2:
    return n
    return fib(n - 1) + fib(n - 2)
  5. Валидация, транзакции, метрики:

    • Оформление "обёрток" вокруг бизнес-функций.

Технические детали и best practices:

  • Использовать functools.wraps для сохранения метаданных оригинальной функции (имя, docstring, аннотации), иначе диагностика и документация ломаются.
from functools import wraps

def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
  • Декораторы с параметрами:
    • Декоратор, который сам принимает аргументы, реализуется как фабрика декораторов.
from functools import wraps

def repeat(times: int):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def hello():
print("hi")

hello()
# "hi" будет выведено 3 раза

Аналогия с Go (как концепт):

В Go нет синтаксического сахара для декораторов, но идея широко используется:

  • Оборачивание HTTP-хендлеров, клиентов, репозиториев дополнительным поведением (логирование, метрики, трейсинг, ретраи).
  • Это тот же "decorator pattern", только реализованный явно через функции/структуры.

Пример "декоратора" для HTTP-хендлера в Go:

type Middleware func(http.Handler) http.Handler

func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}

func Hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}

func main() {
http.Handle("/hello", LoggingMiddleware(http.HandlerFunc(Hello)))
http.ListenAndServe(":8080", nil)
}

Ключевой вывод:

  • Декораторы — это удобный, декларативный способ расширять поведение функций/методов/классов, не нарушая принцип открытости/закрытости и не дублируя код.
  • Умение:
    • объяснить, как они работают;
    • написать простой декоратор;
    • показать реальные сценарии (логирование, авторизация, кеширование) — ожидается как базовый навык.

Вопрос 11. Как устроены dict и set в Python и какова сложность доступа к элементам?

Таймкод: 00:09:46

Ответ собеседника: неправильный. Кандидат верно описал назначение set и dict (уникальные значения и пары ключ-значение), но неверно оценил сложность доступа, указав линейную для dict и константную для set, не показав понимания их общей хеш-структуры.

Правильный ответ:

И dict, и set в Python реализованы на основе хеш-таблиц. Это фундаментально важно для оценки их производительности и понимания поведения.

Ключевые свойства:

  • dict:

    • Хранит пары ключ-значение.
    • Ключи уникальны.
    • Доступ по ключу организован через хеш-таблицу.
  • set:

    • Хранит только уникальные элементы (по сути, только ключи без значений).
    • Реализован на той же идее хеш-таблицы, что и dict, но упрощённо.
  1. Внутреннее устройство на уровне концепции

Упрощённо механизм таков:

  • При добавлении элемента:

    • Для ключа (или элемента в set) вычисляется хеш-функция: hash(key).
    • На основе хеша вычисляется индекс ячейки в массиве (bucket) через взятие остатка по размеру таблицы.
    • В эту ячейку (или ближайшую свободную) помещается запись.
  • При поиске элемента:

    • Снова вычисляется hash(key).
    • Определяется индекс.
    • Сравниваются:
      • сохранённый хеш,
      • затем (при совпадении хеша) сам ключ (операция равенства), чтобы избежать коллизий.
  • При коллизиях (разные ключи с одинаковым индексом или хешем):

    • Python использует открытую адресацию (разные варианты probing — поиск следующей свободной позиции по определённому алгоритму).
    • Таблица периодически перераспределяется (resize/rehash), чтобы сохранять низкий коэффициент заполнения (load factor) и O(1) амортизированную сложность операций.
  1. Асимптотическая сложность

Амортизированно (в среднем случае при адекватном количестве коллизий):

  • Для dict:

    • Доступ по ключу: получение value = d[key] — O(1).
    • Вставка: d[key] = value — O(1).
    • Проверка наличия ключа: key in d — O(1).
    • Удаление по ключу — O(1).
  • Для set:

    • Проверка принадлежности: x in s — O(1).
    • Добавление: s.add(x) — O(1).
    • Удаление: s.remove(x) — O(1) (если элемент есть).

В худшем случае (патологические коллизии):

  • Сложность может деградировать до O(n), но:
    • встроенный алгоритм хеширования и перераспределения спроектирован так, чтобы в реальных системах это было крайне редким случаем;
    • при нормальном распределении хешей операции считаются O(1) амортизированно.

Важно:

  • И dict, и set используют хеширование и имеют схожие характеристики.
  • Ошибка думать, что dict — O(n) по доступу, а set — O(1). Оба — хеш-структуры, оба рассчитаны на O(1) доступ.
  1. Особенности dict в современных версиях Python

Современный CPython (3.6+ фактически, 3.7+ гарантированно):

  • dict сохраняет порядок вставки ключей.
  • Внутри используется:
    • массив индексов (hash table),
    • компактный массив записей (keys/values),
    • что позволяет:
      • оптимизировать память,
      • сохранить порядок и быстрый доступ.

Пример:

d = {}
d["a"] = 1
d["b"] = 2
d["c"] = 3

print(d) # {'a': 1, 'b': 2, 'c': 3} — порядок вставки сохраняется

Но:

  • Порядок — это дополнительная гарантия реализации, не влияет на асимптотику доступа.
  1. Особенности set
  • Реализован поверх хеш-таблицы, концептуально близок к словарю:
    • каждый элемент — это, грубо, "ключ без значения".
  • Требования к элементам:
    • элементы должны быть хешируемыми (immutable или с корректной реализацией __hash__ и __eq__);
    • изменение объекта, участвующего как элемент set или ключ dict, так что меняется его хеш/равенство, приводит к некорректному поведению.
  1. Требования к ключам (dict) и элементам (set)

Ключ (или элемент множества) должен:

  • быть хешируемым: иметь стабильный __hash__ по времени жизни в коллекции;
  • иметь согласованный __eq__:
    • если a == b, то hash(a) == hash(b) (обратное не обязательно).

Нарушение этих контрактов:

  • приводит к невозможности найти элемент или ключ,
  • логическим багам.
  1. Аналогия с Go (для контраста мышления)

В Go:

  • map[K]V реализован как хеш-таблица с амортизированной O(1) сложностью для get/set/delete.
  • Требования к ключам:
    • ключ должен быть comparable (в том числе участвовать в операциях ==, !=);
    • нет произвольной переопределяемости hash/eq, как в Python, что упрощает гарантии.

Сходство:

  • Python dict / set и Go map — концептуально одна и та же идея: хеш-таблица для эффективного доступа.

Краткий вывод:

  • dict:

    • ассоциативный массив: ключ → значение;
    • реализован как хеш-таблица;
    • доступ, вставка, проверка наличия по ключу — O(1) амортизированно.
  • set:

    • множество уникальных элементов;
    • реализован как хеш-таблица;
    • добавление, удаление, проверка наличия — O(1) амортизированно.
  • И dict, и set:

    • используют хеширование,
    • требуют корректных хешируемых ключей/элементов,
    • могут деградировать до O(n) в теории, но в нормальных условиях работают как O(1).

Вопрос 12. Как устроены dict и set в Python и какова их временная сложность доступа к элементам?

Таймкод: 00:09:46

Ответ собеседника: неполный. Кандидат верно описал, что set хранит уникальные значения, а dict — пары ключ-значение. Изначально ошибочно назвал сложность доступа к dict линейной, затем после подсказок начал говорить о зависимости от числа элементов в бакете. В конце уловил идею, что обе структуры реализованы на хеш-таблицах, упомянул O(1) в лучшем случае и O(N) в худшем, но объяснил неуверенно и противоречиво.

Правильный ответ:

Для эффективной разработки важно чётко понимать, как устроены базовые структуры данных. В Python и dict, и set основаны на хеш-таблицах и имеют схожую временную сложность операций.

Общее устройство:

  • dict:

    • Хранит пары "ключ → значение".
    • Ключи уникальны.
    • Доступ к значению по ключу реализован через хеш-таблицу.
  • set:

    • Хранит только уникальные элементы.
    • Фактически похож на dict, но хранит только ключи (значение концептуально "пустое").

Как это работает (концептуально):

  1. При вставке:

    • Вычисляется hash(key).
    • По хешу определяется индекс в массиве бакетов.
    • Элемент размещается в соответствующей позиции или ближайшей свободной (используется открытая адресация и probing-стратегия).
    • При росте заполненности таблица автоматически расширяется (resize), чтобы сохранять низкую плотность и O(1) доступ.
  2. При поиске:

    • Снова вычисляется hash(key).
    • По индексу ищется запись:
      • сначала по хешу,
      • затем (при совпадении хеша) по == (для разрешения коллизий).
    • В случае коллизий алгоритм просматривает последовательность позиций (probe sequence), пока не найдёт ключ или свободную ячейку.
  3. При коллизиях:

    • Используются детерминированные стратегии обхода (open addressing).
    • Хорошее распределение хешей и поддержание разумного load factor делают количество проб небольшим.

Асимптотика:

  • Амортизированная (средний случай при нормальных хешах):

    • dict:
      • доступ по ключу: d[k], k in d — O(1)
      • вставка: d[k] = v — O(1)
      • удаление: del d[k] — O(1)
    • set:
      • проверка принадлежности: x in s — O(1)
      • добавление: s.add(x) — O(1)
      • удаление: s.remove(x) — O(1)
  • Худший случай:

    • O(N), если почти все ключи попали в коллизию или произошли атаки на хеши.
    • На практике за счёт:
      • хорошего хеширования,
      • перераспределений,
      • рандомизации
      • такие случаи редки, и структуры считаются O(1) амортизированно.

Ключевой вывод:

  • И dict, и set — хеш-таблицы с амортизированной O(1) сложностью для доступа/вставки/проверки.
  • Нельзя считать, что dict — O(N) по доступу "по определению", это неправильно.
  • У обеих структур один и тот же фундаментальный механизм, с разницей в том, что dict хранит пары ключ-значение, а set — только ключи.

Требования к ключам и элементам:

  • Ключ dict и элемент set должны быть:
    • хешируемыми (имеют стабильный __hash__ пока используются в структуре),
    • с согласованным __eq__:
      • если a == b, то hash(a) == hash(b).
  • Нарушение этого контракта (например, изменяемые объекты как ключи) ведет к некорректному поведению (невозможность найти элемент и логические баги).

Дополнительные детали:

  • Современный CPython (3.7+):
    • гарантирует сохранение порядка вставки для dict.
    • Это реализовано отдельными структурами (индексы + массив записей), поверх всё ещё хеш-таблица.
    • Порядок — это гарантия API, но асимптотика доступа по ключу остаётся O(1) амортизированно.

Аналогия с Go:

  • В Go встроенный map[K]V реализован как хеш-таблица:
    • доступ, вставка, удаление — O(1) амортизированно;
    • ключи должны быть сравнимыми (comparable).
  • Концептуально то же, что dict/set в Python, но с жёстче заданной моделью типов.

Правильная формулировка для интервью:

  • dict и set в Python реализованы через хеш-таблицы.
  • В среднем:
    • операции доступа, вставки, удаления и проверки наличия — O(1) амортизированно.
  • В худшем случае возможна деградация до O(N), но это редкая и нежелательная ситуация.
  • dict хранит пары ключ-значение, set хранит только уникальные ключи.

Вопрос 13. Объясни принцип работы сборщика мусора в Python.

Таймкод: 00:12:48

Ответ собеседника: неполный. Кандидат описывает общий механизм: периодический проход по памяти, поиск неиспользуемых объектов и их удаление. Не упоминает ключевую роль подсчёта ссылок и отдельный механизм для обработки циклических ссылок.

Правильный ответ:

В Python (на примере CPython, т.к. это стандартная и наиболее важная реализация) сборка мусора основана на двух ключевых механизмах:

  1. Подсчёт ссылок (reference counting) — основной и немедленный.
  2. Дополнительный циклический сборщик (cycle GC) — для обнаружения и очистки объектных циклов, которые не может освободить один подсчёт ссылок.

Важно понимать оба, чтобы корректно оценивать поведение памяти, утечки и особенности долгоживущих сервисов.

Основной механизм: подсчёт ссылок

Каждый объект в CPython хранит счётчик ссылок ob_refcnt.

Идея:

  • Когда создаётся новый объект — его счетчик ссылок = 1.
  • Когда создаётся новая ссылка на объект (присваивание, передача в список, dict, func-аргумент и т.п.) — счетчик увеличивается.
  • Когда ссылка уничтожается или переназначается — счетчик уменьшается.
  • Когда счётчик ссылок объекта становится 0:
    • объект немедленно освобождается,
    • его память возвращается менеджеру памяти.

Пример (концептуально):

a = [1, 2, 3]  # refcount(a) = 1
b = a # refcount(a) = 2
del b # refcount(a) = 1
del a # refcount(a) = 0 -> список освобождается сразу

Плюсы:

  • Предсказуемость: большинство объектов освобождаются сразу, как только становятся недостижимыми.
  • Это важно для файловых дескрипторов, соединений, блокировок и т.п., когда мы полагаемся на своевременное освобождение.

Минус:

  • Подсчёт ссылок не умеет справляться с циклическими ссылками.

Проблема: циклические ссылки

Если два (или более) объекта ссылаются друг на друга, но больше недостижимы из "корней" программы (глобальные переменные, стек вызовов и т.п.), то их refcount не упадет до нуля, и один только подсчёт ссылок не освободит их.

Пример:

class Node:
def __init__(self):
self.ref = None

a = Node()
b = Node()
a.ref = b
b.ref = a

# После этого:
del a
del b
# Узлы продолжают ссылаться друг на друга.
# Их refcount > 0, хотя доступны они уже только друг через друга.

Чтобы такие циклы не приводили к утечкам, в CPython есть отдельный циклический сборщик.

Циклический сборщик (cycle GC)

Он реализован в модуле gc и работает поверх подсчёта ссылок.

Ключевые идеи:

  • Отслеживает только "container objects":
    • объекты, которые могут содержать ссылки на другие объекты: list, dict, set, tuple (если внутри есть объекты), классы, инстансы, и т.д.
  • Организует их по поколениям (generational GC):
    • поколение 0: новые объекты;
    • поколение 1, 2: объекты, пережившие несколько циклов сборки.
  • Предположение: "большинство объектов живут недолго" (как и в большинстве современных GC).

Алгоритм (упрощённо):

  1. Периодически, когда количество аллокаций/объектов превышает порог, запускается сборка для "младшего" поколения.
  2. GC просматривает объекты-контейнеры, строит граф ссылок.
  3. Пытается определить объекты, недостижимые из "корней" (глобальные переменные, стек, регистры и т.п.).
  4. Объекты, которые образуют цикл и недостижимы извне, помечаются как мусор.
  5. Они освобождаются, даже если их refcount > 0 из-за внутренних циклических ссылок.

Сложности с финализаторами (__del__):

  • Если объекты в цикле имеют методы __del__, стандартный механизм не всегда может безопасно определить порядок уничтожения.
  • В старых версиях такие объекты могли попадать в "uncollectable" (неподлежащий сборке мусор).
  • Современный CPython улучшил поведение, но общее правило:
    • Стараться избегать сложной логики в __del__.
    • Для управления ресурсами использовать контекстные менеджеры (with) и contextlib.

Управление и настройка (модуль gc):

Можно:

  • Посмотреть пороги и статистику:
    import gc
    print(gc.get_threshold())
    print(gc.get_count())
  • Временно отключать GC (например, в performance-критичных местах):
    gc.disable()
    # ... код ...
    gc.enable()
  • Принудительно запускать сборку:
    gc.collect()

Практические выводы:

  1. Немедленное освобождение:

    • В отличие от многих языков с чистым "stop-the-world" GC, в CPython большинство объектов освобождается сразу при обнулении ссылок.
    • Это удобно для управления ресурсами, но важно помнить: это деталь реализации CPython, а не абстрактная гарантия всех реализаций Python.
  2. Утечки памяти:

    • Основные источники проблем — не GC как таковой, а:
      • глобальные структуры, кеши, синглтоны;
      • длинноживущие ссылки на объекты (замыкания, лямбды, классы, кэши);
      • циклы с __del__.
    • Для диагностики полезны:
      • gc.get_objects(), gc.get_referrers(); сторонние инструменты (objgraph и т.п.).
  3. Сравнение мышления с Go:

  • В Go:
    • используется три-color mark-and-sweep, concurrent, non-moving GC (в современных версиях);
    • программист не управляет подсчётом ссылок вручную, GC периодически обходит граф объектов.
  • В CPython:
    • основа — reference counting + дополнительный cycle GC.
  • Для разработчика, работающего с обоими языками, важно понимать:
    • в Python многие ресурсы освобождаются детерминированно (через refcount),
    • в Go — недетерминированно, но с мягкими паузами и concurrent GC.

Краткая формулировка для интервью:

  • В CPython сборка мусора основана на подсчёте ссылок: когда счётчик ссылок объекта падает до 0, он сразу уничтожается.
  • Для обнаружения циклических ссылок используется отдельный циклический сборщик с поколениями.
  • В среднем это даёт предсказуемое освобождение памяти + защиту от утечек из-за циклов.

Вопрос 14. Как наиболее простым способом удалить дубликаты из списка в Python?

Таймкод: 00:13:24

Ответ собеседника: правильный. Предложил преобразовать список в множество и затем обратно в список.

Правильный ответ:

Самый простой способ убрать дубликаты из списка в Python — преобразовать его во множество (set), а затем обратно в список:

items = [1, 2, 2, 3, 3, 3]
unique = list(set(items))
# unique может быть, например, [1, 2, 3]

Но важно понимать нюансы и уметь выбирать способ в зависимости от требований.

Ключевые моменты:

  1. Базовое решение через set:
  • Плюсы:
    • Простое, короткое, наглядное.
    • Операции на множестве работают амортизированно за O(1), общее время — O(N).
  • Минус:
    • Не сохраняется порядок элементов (до Python 3.7 порядок dict/set был не гарантирован; даже сейчас на порядок set полагаться не следует).
    • Подходит, если порядок не важен.
  1. Сохранение порядка элементов:

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

Вариант 1 (явный цикл):

def unique_preserve_order(seq):
seen = set()
res = []
for x in seq:
if x not in seen:
seen.add(x)
res.append(x)
return res

items = [3, 1, 2, 3, 2, 1]
print(unique_preserve_order(items)) # [3, 1, 2]
  • Время: O(N) амортизированно.
  • Память: O(N) на множество и результирующий список.
  • Порядок первых вхождений сохраняется.

Вариант 2 (Python 3.7+ с dict):

items = [3, 1, 2, 3, 2, 1]
unique = list(dict.fromkeys(items))
# dict сохраняет порядок ключей → результат: [3, 1, 2]

Это лаконичный и идиоматичный способ для "уникальных с сохранением порядка".

  1. Требования к элементам:

Во всех вариантах:

  • Элементы должны быть хешируемыми (для использования в set/dict).
  • Если элементы не хешируемы (например, списки), можно:
    • привести их к хешируемому виду (tuple),
    • или использовать более сложную логику (например, сериализацию или сравнение по ключу).
  1. Сопоставление с Go-мышлением:

В Go нет встроенного множества, но аналог можно сделать на базе map:

func UniqueInts(nums []int) []int {
seen := make(map[int]struct{})
res := make([]int, 0, len(nums))
for _, v := range nums {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
res = append(res, v)
}
}
return res
}

Идея та же:

  • map/set на основе хеш-таблицы,
  • O(N) по времени,
  • контроль над порядком (порядок в результирующем слайсе определяется порядком обхода исходного списка).

Итог:

  • "Простой способ" для интервью:
    • list(set(lst)) — если порядок не важен.
  • "Правильный и практичный способ":
    • list(dict.fromkeys(lst)) или цикл с seen = set() — если порядок важен и нужно предсказуемое поведение.

Вопрос 15. Как правильно разделить датасет на обучающую, валидационную и тестовую выборки и какие пропорции использовать?

Таймкод: 00:13:46

Ответ собеседника: неполный. Кандидат верно отмечает необходимость разделения на train/val/test и ссылается на train_test_split, но допускает неточность про "первые элементы", не акцентирует важность рандомизации и стратификации. Пропорции 80/20 и 90/10 называет, зависимость от размера датасета в целом описывает верно, но без чёткой структуры.

Правильный ответ:

При разделении датасета ключевые цели:

  • Оценивать обобщающую способность модели на данных, которые она не видела при обучении.
  • Избежать утечек данных (data leakage).
  • Иметь репрезентативные подвыборки по целевой переменной и важным признакам.

Обычно мы оперируем тремя наборами:

  • Обучающая выборка (train): обучение и подбор параметров модели.
  • Валидационная (validation): подбор гиперпараметров, выбор архитектуры/регуляризации, ранняя остановка.
  • Тестовая (test): финальная, "честная" оценка качества, не используется в процессе настройки.

Базовые пропорции:

Типичные варианты (зависят от объёма данных):

  • Малый/средний датасет:
    • 60% train / 20% val / 20% test
    • 70% train / 15% val / 15% test
  • Большой датасет (сотни тысяч+ объектов):
    • 80% train / 10% val / 10% test
    • 90% train / 5% val / 5% test
  • Очень большой датасет:
    • Можно взять относительно маленький, но репрезентативный validation/test (например, по 1–5%), остальное — train.

Ключевые моменты:

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

Рандомизация:

  • Нельзя просто "брать первые N%" для train, а остаток — в test, если данные упорядочены (по времени, по источнику, по пользователям и т.д.).
  • Если данные перемешаны и не имеют временной или иной структуры:
    • используем случайное разбиение (random shuffle).
  • Если данные временные:
    • нельзя перемешивать произвольно.
    • используется time-based split:
      • train — более ранний период;
      • val/test — более поздние периоды.
    • Это имитирует реальный сценарий предсказания будущего по прошлому.

Стратификация:

  • Если задача классификации:
    • использовать стратифицированное разбиение по целевой переменной:
      • чтобы доля классов в train/val/test примерно совпадала с исходной.
    • Особенно важно при несбалансированных классах.

Data leakage (утечка данных):

При любом разбиении критично:

  • Любые операции, зависящие от статистик данных (масштабирование, нормализация, PCA, отбор признаков по корреляции, целевой encoding и т.п.) обучаются только на train.
  • Потом те же трансформации применяются к val/test.
  • Нельзя:
    • считать среднее/стандартное отклонение/квантили/частоты по всему датасету и потом делить;
    • использовать информацию о целевой переменной из test/val при построении признаков train.
  • Для временных рядов:
    • особенно строго следить за тем, чтобы "информация из будущего" не попадала в train.

Практический пример (Python, sklearn):

Разделение train/test:

from sklearn.model_selection import train_test_split

X_train, X_temp, y_train, y_temp = train_test_split(
X, y,
test_size=0.3, # 30% во временный набор
random_state=42,
stratify=y # для классификации
)

# Теперь делим временный набор на val и test пополам
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp,
test_size=0.5, # половина от 30% -> 15%/15%
random_state=42,
stratify=y_temp
)

Результат: 70% / 15% / 15% с рандомизацией и стратификацией.

Для временных рядов (упрощенно):

# Данные отсортированы по времени
n = len(X)
train_end = int(n * 0.7)
val_end = int(n * 0.85)

X_train, y_train = X[:train_end], y[:train_end]
X_val, y_val = X[train_end:val_end], y[train_end:val_end]
X_test, y_test = X[val_end:], y[val_end:]

Тут:

  • Никакого перемешивания.
  • Валидация и тест — более поздние периоды.

Связь с инженерным мышлением (важная практика):

  • В проде критичен не только сплит, но и воспроизводимость:
    • фиксировать random_state;
    • документировать стратегию разбиения;
    • не переиспользовать тестовую выборку для многократного "тюнинга", иначе она перестает быть честной.
  • При сложных продуктах:
    • часто делают k-fold cross-validation на train для подбора гиперпараметров;
    • выделяют отдельный hold-out test, который не трогают до финальной оценки.

Краткая формулировка для интервью:

  • Разбиваем на train/val/test.
  • Используем случайное разбиение (shuffle) с фиксацией seed, кроме временных рядов — там time-based split.
  • Для классификации — стратификация по целевой.
  • Типичные пропорции:
    • 70/15/15, 80/10/10 или 90/5/5 — в зависимости от размера данных.
  • Любые преобразования, зависящие от данных, обучаются только на train, затем применяются к val/test, чтобы избежать утечки данных.

Вопрос 16. Почему при маленьком датасете долю валидационной выборки имеет смысл делать относительно больше и какие проблемы возникают при слишком маленькой валидации?

Таймкод: 00:16:03

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

Правильный ответ:

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

  • если сделать валидационную выборку слишком маленькой,
  • метрики на ней будут:
    • нестабильными,
    • чувствительными к конкретным примерам,
    • плохо отражать реальную обобщающую способность модели.

Почему доля валидации должна быть относительно больше:

  1. Статистическая устойчивость:

    • Метрики (accuracy, F1, ROC-AUC, MAPE и т.д.) являются статистическими оценками.
    • При очень малом числе примеров:
      • одна-две аномальные точки или смещение по классам могут сильно исказить результат.
    • Более крупная валидация уменьшает дисперсию оценки: мы получаем более стабильный сигнал, на основе которого выбираем модель и гиперпараметры.
  2. Репрезентативность распределения:

    • В маленькой валидации могут:
      • не встретиться редкие классы,
      • не попасть важные паттерны,
      • не отражаться реальные пропорции.
    • В результате модель может казаться хорошей на валидации, но проваливаться на реальных данных.
  3. Риск оверфита на валидацию:

    • Если валидация маленькая, а вы многократно перебираете гиперпараметры/архитектуры:
      • модель (или ваш процесс выбора) "подгоняется" под конкретные несколько десятков/сотен примеров.
      • Вы бессознательно начинаете переобучаться на validation set, так же как можно переобучиться на train.
    • Чем меньше выборка, тем легче её "заучить" и тем менее честный сигнал она даёт.

Какие проблемы при слишком маленькой валидации:

  • Шумные метрики:

    • Нельзя уверенно отличить:
      • "эта модель действительно лучше"
      • от "нам просто повезло на этих 30 примерах".
  • Неправильный выбор модели:

    • Можно выбрать архитектуру/гиперпараметры, которые случайно показали лучший результат на маленькой валидации, но в реальности хуже.
  • Ложное чувство уверенности:

    • Маленькая выборка даёт красивые числа, но они статистически незначимы.

Как действовать на практике при малом датасете:

  • Увеличивать относительный размер validation/test:

    • вместо, скажем, 90/5/5 можно сделать 70/15/15 или близкие схемы;
    • важно не абсолютное процентное значение, а достаточное количество примеров для устойчивых метрик.
  • Использовать перекрёстную проверку (k-fold cross-validation):

    • Разбиваем данные на k фолдов.
    • Для каждой модели делаем k прогонов (train на k-1 фолдах, validate на оставшемся).
    • Усредняем метрики.
    • Это:
      • повышает устойчивость оценки,
      • позволяет эффективнее использовать небольшой датасет,
      • уменьшает зависимость от конкретного случайного сплита.
  • Следить за стратификацией:

    • Особенно при несбалансированных классах:
      • маленькая non-stratified валидация легко "теряет" редкий класс,
      • метрики становятся бессмысленными.

Краткая формулировка:

  • При маленьком датасете:
    • нужна относительно более крупная и стратифицированная валидационная выборка,
    • чтобы оценка качества была статистически значимой и не скакала от случайных выбросов.
  • Слишком маленькая валидация:
    • даёт шумные и нерепрезентативные метрики,
    • повышает риск выбрать неправильную модель,
    • стимулирует переобучение на несколько случайных наблюдений.

Вопрос 17. На что обратить внимание при подготовке многоклассового датасета (например, с тремя классами) перед обучением модели?

Таймкод: 00:18:23

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

Правильный ответ:

При подготовке многоклассового датасета (3+ классов) важно не ограничиваться только проверкой баланса. Нужно системно пройтись по нескольким аспектам, чтобы модель обучалась корректно и метрики были честными.

Ключевые моменты:

  1. Баланс классов и репрезентативность
  • Проверить распределение классов:
    • нет ли ситуации, когда один класс занимает 80–95% данных, а остальные почти не представлены;
    • для всех классов должно быть достаточно примеров для обучения и валидации.
  • Если сильный дисбаланс:
    • модель может игнорировать редкие классы и “выигрывать” по accuracy, предсказывая только мажоритарный класс.
  • Подходы:
    • стратифицированный train/val/test split, чтобы сохранить распределение классов во всех выборках;
    • oversampling редких классов (например, случайное или продвинутое вроде SMOTE);
    • undersampling мажоритарных классов;
    • использование взвешенных лоссов (class weights) для компенсации дисбаланса;
    • метрики, чувствительные к балансу: macro/micro F1, balanced accuracy, per-class recall.
  1. Корректная разметка и качество таргета
  • Проверить:
    • нет ли перепутанных меток;
    • нет ли классов с дублирующим смыслом;
    • нет ли класса “свалка всего” без чётких критериев.
  • Минимум:
    • ручная проверка части данных по каждому классу;
    • одинаковые правила разметки для всех источников данных.
  1. Разделение на выборки без утечки данных
  • Строго следить, чтобы объекты, логически связанные, не разошлись по train/val/test так, что создаётся утечка:
    • один и тот же пользователь (или устройство, или сессия) не должен одновременно быть и в train, и в test;
    • одинаковые или почти одинаковые элементы (дубликаты) не должны оказаться в train и test в разных классах.
  • Для многоклассовых задач особенно важно, чтобы:
    • все классы были представлены в train и val/test;
    • разделение было стратифицированным:
      • в sklearn: StratifiedKFold, train_test_split(..., stratify=y).
  1. Качество признаков и информативность для всех классов
  • Убедиться, что признаки позволяют отличать все классы, а не только мажоритарный:
    • визуализация (t-SNE/UMAP) для sanity-check;
    • базовый анализ: корреляции, распределения признаков по классам.
  • Важно:
    • отсутствие признаков, “подсматривающих” истинный класс напрямую (data leakage);
    • отсутствие признаков, завязанных на индексы, имена файлов, сырой ID и т.п., если они коррелируют с классом случайно.
  1. Кодирование таргета и классов
  • Явно определить:
    • mapping между именами классов и их ID (например: {0: "class_A", 1: "class_B", 2: "class_C"}).
  • Следить за:
    • консистентностью этого mapping во всех стадиях пайплайна: обучение, валидация, инференс;
    • правильной работой one-vs-rest / softmax при обучении.
  1. Метрики для многоклассовой задачи

Ещё до обучения стоит определить:

  • какие метрики релевантны:
    • accuracy может быть недостаточной;
    • macro-F1 (чувствителен к редким классам),
    • weighted-F1,
    • per-class precision/recall.
  • Анализировать confusion matrix:
    • помогает увидеть, какие классы путаются между собой;
    • позорный кейс: модель всегда предсказывает один класс → высокая accuracy при полном игноре других.
  1. Практический пример на Python (стратифицированный сплит)
from sklearn.model_selection import train_test_split
import numpy as np

# X — признаки, y — метки классов (0, 1, 2)
X_train, X_temp, y_train, y_temp = train_test_split(
X, y,
test_size=0.3,
random_state=42,
stratify=y # сохраняем распределение классов
)

X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp,
test_size=0.5,
random_state=42,
stratify=y_temp
)

# Быстрая проверка распределения
def class_dist(name, labels):
unique, counts = np.unique(labels, return_counts=True)
print(name, dict(zip(unique, counts)))

class_dist("train", y_train)
class_dist("val", y_val)
class_dist("test", y_test)
  1. Мышление с точки зрения инженерии
  • Не ограничиваться абстрактным "баланс классов":
    • проверить распределения;
    • правильно разбить;
    • заложить выбор адекватных метрик;
    • убедиться, что пайплайн честный (без утечек).
  • Особенно при маленьком числе примеров для некоторых классов:
    • использовать стратифицированный k-fold cross-validation для устойчивой оценки;
    • внимательно смотреть на per-class метрики, а не только на средние значения.

Кратко:

  • Контролировать баланс и репрезентативность классов.
  • Делать стратифицированное разбиение на train/val/test.
  • Избегать утечки данных.
  • Выбирать правильные метрики, учитывающие многоклассовую природу и дисбаланс.
  • Проверять качество разметки и признаки для всех классов, а не только для самого частого.

Вопрос 18. Что можно сделать при несбалансированном датасете для выравнивания классов?

Таймкод: 00:19:22

Ответ собеседника: неполный. Кандидат интуитивно предлагает уменьшать доминирующий класс или увеличивать редкий, но путает техники: смешивает заполнение признаков и балансировку классов, не называет явно oversampling/undersampling, data augmentation, взвешенные лоссы и другие стандартные подходы.

Правильный ответ:

При несбалансированном датасете цель — не просто “сделать равные количества”, а добиться того, чтобы модель:

  • не игнорировала редкие классы;
  • хорошо различала все целевые классы, особенно важные бизнес-классы (fraud, отказ, критические события и т.п.);
  • не переобучилась на искусственно созданных данных.

Ключевые стратегические подходы (часто комбинируются):

  1. Undersampling (уменьшение мажоритарного класса)

Идея:

  • Случайно (или осмысленно) уменьшить количество объектов доминирующего класса, чтобы приблизить баланс.

Плюсы:

  • Быстро и просто.
  • Уменьшает размер данных → ускоряет обучение.

Минусы:

  • Потеря информации:
    • выбрасываем реальные данные, потенциально полезные для границы решений.
  • Не всегда приемлемо при небольших датасетах.

Пример (Python):

from sklearn.utils import resample
import numpy as np

X_major = X[y == 0]
X_minor = X[y == 1]

y_major = y[y == 0]
y_minor = y[y == 1]

X_major_down, y_major_down = resample(
X_major, y_major,
replace=False,
n_samples=len(y_minor),
random_state=42,
)

X_balanced = np.vstack([X_major_down, X_minor])
y_balanced = np.concatenate([y_major_down, y_minor])
  1. Oversampling (увеличение миноритарного класса)

Идея:

  • Добавить больше примеров редких классов:
    • простое дублирование (random oversampling),
    • генерация синтетических примеров (SMOTE, ADASYN и др.).

Плюсы:

  • Сохраняем все данные мажоритарного класса.
  • Даем модели больше сигналов по редким классам.

Минусы:

  • Простое дублирование может привести к переобучению на редкий класс:
    • модель “зазубрит” несколько одинаковых примеров.
  • Синтетические методы требуют аккуратности:
    • могут создавать неестественные объекты, если признаки сложные или нечисловые.

Простой oversampling:

from sklearn.utils import resample

X_minor_upsampled, y_minor_upsampled = resample(
X_minor, y_minor,
replace=True,
n_samples=len(y_major),
random_state=42,
)

X_balanced = np.vstack([X_major, X_minor_upsampled])
y_balanced = np.concatenate([y_major, y_minor_upsampled])
  1. Data augmentation (особенно для изображений, текста, аудио)

Идея:

  • Генерировать новые, правдоподобные примеры для редких классов:
    • изображения: геометрические трансформации, шум, изменение яркости/контраста;
    • текст: перефразирование, синонимы, back-translation;
    • аудио: шум, питч, скорость.
  • Это форма осмысленного oversampling.

Плюсы:

  • Повышает разнообразие данных.
  • Может улучшить обобщающую способность модели.

Минусы:

  • Нужна доменная экспертиза, чтобы не исказить семантику.
  • Некачественный augmentation даст шум и ухудшит модель.
  1. Взвешенные лоссы (class weights)

Идея:

  • Вместо изменения данных — изменить функцию потерь:
    • ошибки на редком классе штрафуются сильнее.
  • В бинарной и многоклассовой классификации:
    • добавляем веса классов в loss (например, в логистической регрессии, деревьях, нейросетях).

Плюсы:

  • Не меняем распределение данных.
  • Гибкий и часто предпочтительный способ для продакшена.
  • Хорошо поддерживается во многих фреймворках (sklearn, XGBoost, PyTorch, TF).

Минусы:

  • Требует аккуратного подбора весов:
    • слишком большие веса → переобучение/нестабильность.

Пример (sklearn):

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(class_weight="balanced")
clf.fit(X_train, y_train)

Или вручную:

class_weight = {
0: 1.0, # мажоритарный
1: 5.0, # миноритарный
2: 5.0,
}
clf = LogisticRegression(class_weight=class_weight)
  1. Изменение порогов классификации

Идея:

  • Для вероятностных моделей (логистическая регрессия, градиентный бустинг, нейросети):
    • не использовать фиксированный порог 0.5 для всех классов.
    • подобрать пороги отдельно для классов (в one-vs-rest или через калибровку), исходя из:
      • бизнес-стоимости ошибок,
      • precision/recall trade-off.

Плюсы:

  • Особо полезно, если важно максимально ловить редкий класс (fraud detection, дефекты).

Минусы:

  • Требует стабильных вероятностных оценок и отдельной настройки.
  1. Выбор правильных метрик

При несбалансированных данных:

  • accuracy почти бесполезна.
  • Нужны:
    • precision, recall, F1-score (macro/weighted),
    • ROC-AUC (в т.ч. macro/micro),
    • PR-AUC для редких классов,
    • per-class метрики,
    • confusion matrix.

Это не “выравнивает” классы, но меняет критерий, по которому принимаются решения об архитектуре / гиперпараметрах / порогах.

  1. Корректный split и стратификация

Обязательно:

  • Стратифицированный train/val/test split:
    • чтобы редкие классы были представлены в каждой выборке.
  • Никаких утечек данных:
    • нельзя, чтобы один и тот же объект/сущность попадал в train и test под разными видами.

Пример:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
stratify=y
)
  1. Комбинации подходов

На практике часто комбинируют:

  • stratified split + class weights;
  • oversampling редких классов в train + взвешенный loss;
  • data augmentation + разумные пороги + корректные метрики.

Главное:

  • Делать балансировку только по train:
    • validation и test должны отражать реальное распределение, иначе вы получите “красивые, но ложные” метрики.
  • Не смешивать задачи:
    • заполнение пропусков и генерация признаков — это не oversampling классов;
    • важно чётко разделять балансировку классов и обработку feature-level пропусков.

Краткая формулировка для интервью:

  • При несбалансированном датасете используют:
    • undersampling мажоритарного класса,
    • oversampling и/или data augmentation для миноритарных классов,
    • взвешенные функции потерь,
    • корректные метрики (F1, PR-AUC, per-class),
    • стратифицированный split.
  • Балансировку делают на train, а val/test оставляют с реальным распределением, чтобы честно оценить модель.

Вопрос 19. Как увеличить количество примеров миноритарного класса в датасете изображений (например, кошек vs собак)?

Таймкод: 00:20:18

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

Правильный ответ:

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

  1. прямое увеличение (oversampling),
  2. аугментация данных (data augmentation).

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

Основные подходы:

  1. Простое дублирование (naive oversampling)
  • Суть:
    • Просто копируем существующие изображения редкого класса (кошек), чтобы увеличить их долю в train.
  • Плюсы:
    • Реализуется очень просто.
  • Минусы:
    • Лёгкое переобучение:
      • модель “зазубрит” одни и те же изображения;
      • плохо обобщает на новые примеры.
  • Используется как временная мера, но для реальных задач обычно предпочитают осмысленную аугментацию.
  1. Data augmentation (осмысленное расширение данных)

Это правильный и стандартный подход.

Идея:

  • На базе каждого исходного изображения генерировать несколько новых, применяя реалистичные трансформации, сохраняющие класс объекта.

Типичные трансформации:

  • Геометрические:
    • отражение по горизонтали (horizontal flip);
    • небольшие повороты;
    • масштабирование (zoom);
    • сдвиги (shift);
    • кропы (random crop);
    • лёгкая аффинная трансформация.
  • Фотометрические:
    • изменение яркости, контраста, насыщенности;
    • небольшой шум.
  • Пространственные:
    • лёгкое размытие или sharpening, если уместно.

Важно:

  • Трансформации должны быть физически и семантически корректными:
    • отзеркаливание кошки — ок;
    • сильное искажение до неузнаваемости или инверсия классов — нельзя.
  • Для мед. изображений, OCR, некоторых промышленных задач список безопасных трансформаций сильно зависит от домена.

Пример (Python, Keras-style псевдокод):

from tensorflow.keras.preprocessing.image import ImageDataGenerator

datagen = ImageDataGenerator(
rotation_range=15,
width_shift_range=0.1,
height_shift_range=0.1,
zoom_range=0.1,
horizontal_flip=True,
brightness_range=(0.8, 1.2),
fill_mode="nearest",
)

# X_cats — изображения кошек (миноритарный класс)
datagen.fit(X_cats)

# При обучении:
# модель будет видеть различные аугментированные версии кошек
model.fit(datagen.flow(X_cats, y_cats, batch_size=32))

Здесь:

  • генератор динамически создаёт новые варианты изображений на лету;
  • мы эффективно увеличиваем разнообразие примеров кошек без ручного копирования.
  1. Комбинированный подход

На практике часто делают:

  • для миноритарного класса:
    • oversampling + агрессивная (но валидная) аугментация;
  • для мажоритарного:
    • умеренная/симметричная аугментация или без неё;
  • плюс:
    • балансировка лосса (class weights),
    • корректные метрики (per-class F1, macro-F1, confusion matrix).

Ключевые правила:

  • Балансировку (oversampling/augmentation) применяем только к тренировочной выборке:
    • validation и test оставляем с реальным распределением, чтобы честно оценивать модель.
  • Следим за тем, чтобы аугментация не меняла класс и не добавляла нереалистичных артефактов.
  • Важно не только “количество картинок”, но и разнообразие:
    • 10 000 копий одной кошки не равно 10 000 разным кошкам под разными условиями.

Краткая формулировка для интервью:

  • Для увеличения примеров меньшего класса (кошек) используют:
    • oversampling (но простое дублирование ограниченно полезно),
    • data augmentation: отражение, повороты, кропы, изменения яркости/контраста и другие реалистичные трансформации,
    • при необходимости в сочетании с взвешенным лоссом.
  • Все изменения делаются на train, а валидация/тест остаются неизменёнными и отражают реальное распределение данных.

Вопрос 20. Как в pandas заменить и удалить строки с отсутствующими значениями (NaN)?

Таймкод: 00:22:02

Ответ собеседника: неполный. Кандидат упоминает общую идею использовать методы заполнения и удаления, но не называет конкретные функции (fillna, dropna) и не объясняет варианты их применения.

Правильный ответ:

В pandas работа с пропусками (NaN/None) — базовый навык при подготовке данных. Основные операции:

  • найти пропуски,
  • заполнить пропуски осмысленными значениями,
  • удалить строки/столбцы с пропусками — при необходимости.

Ключевые функции:

  • isna() / isnull() — найти пропуски.
  • fillna() — заменить пропуски.
  • dropna() — удалить строки/столбцы с пропусками.
  1. Поиск пропусков
import pandas as pd

df = pd.DataFrame({
"age": [25, None, 30],
"city": ["Moscow", "SPB", None],
})

df.isna() # True/False по ячейкам
df.isna().sum() # количество NaN по столбцам
  1. Удаление строк с пропусками: dropna

Базовый вариант: удалить все строки, где есть хотя бы один NaN:

df_clean = df.dropna()

Опции:

  • how:
    • how="any" (по умолчанию): удаляет строку, если есть хотя бы один NaN.
    • how="all": удаляет строку только если все значения NaN.
  • subset:
    • ограничить проверку конкретными столбцами.

Примеры:

# Удалить строки, где в столбце age пропуск
df_age = df.dropna(subset=["age"])

# Удалить строки, где ВСЕ значения NaN
df_all = df.dropna(how="all")

Важно:

  • Удаление строк уменьшает датасет, может исказить распределения.
  • Используем, если:
    • пропусков мало,
    • или строка без ключевых полей бессмысленна.
  1. Замена пропусков: fillna

fillna() позволяет заменить NaN на заданные значения.

Простые варианты:

# Заполнить все NaN нулями
df_filled = df.fillna(0)

# Заполнить NaN в одном столбце
df["age"] = df["age"].fillna(df["age"].mean()) # числовой столбец: среднее
df["city"] = df["city"].fillna("Unknown") # категориальный: специальное значение

Словарь по столбцам:

df = df.fillna({
"age": df["age"].median(),
"city": "Unknown",
})

Использование методов заполнения по соседним значениям:

  • method="ffill" (forward fill) — тянуть предыдущее значение вниз.
  • method="bfill" (backward fill) — тянуть следующее значение вверх.
df_ffill = df.fillna(method="ffill")
df_bfill = df.fillna(method="bfill")

Аккуратность:

  • ffill/bfill логичны для временных рядов или логически упорядоченных данных.
  • Не использовать слепо, если порядок строк не несёт смысла.
  1. inplace и безопасная практика

Обе функции поддерживают inplace=True, но для чистоты и предсказуемости кода лучше возвращать новый DataFrame:

df = df.fillna(0)          # предпочтительнее, чем df.fillna(0, inplace=True)
df = df.dropna(subset=[...])
  1. Осмысленный выбор стратегии

Продвинутый уровень — не просто знать fillna/dropna, а понимать, когда и как применять:

  • Числовые признаки:

    • среднее/медиана,
    • константа (например, 0 или -1, если это семантически корректно),
    • модельная имputation (KNN, регрессия и т.п.) — для более сложных кейсов.
  • Категориальные признаки:

    • отдельная категория типа "Unknown"/"Missing",
    • наиболее частое значение (mode),
    • осторожно: может смещать статистику.
  • Временные ряды:

    • ffill/bfill, интерполяция (df.interpolate()),
    • важно учитывать физический смысл.
  • Удаление строк:

    • только если пропуски редкие или критически нарушают смысл записи.

Краткая формулировка для интервью:

  • Для удаления строк/столбцов с NaN используется dropna() с опциями how, subset.
  • Для замены пропусков используется fillna():
    • константы, среднее/медиана, мода, ffill/bfill и т.д.
  • Важно:
    • сначала понять природу пропусков,
    • затем выбирать стратегию так, чтобы не вносить искажений в данные и не выкидывать ценные примеры без необходимости.

Вопрос 21. Какие алгоритмы машинного обучения ты знаешь и какие из них уместно применять на практике?

Таймкод: 00:22:21

Ответ собеседника: неполный. Перечисляет KNN, линейную регрессию, линейную классификацию, деревья решений, случайный лес и градиентный бустинг, но неуверенно. Из практики называет в основном учебные задачи; продвинутые алгоритмы (случайный лес, бустинг) фактически не применял.

Правильный ответ:

Корректный и зрелый ответ на этот вопрос — не просто список алгоритмов, а понимание:

  • их классов (линейные, деревья, ансамбли, инстанс-базированные, вероятностные, нейросетевые),
  • задач, для которых они подходят,
  • сильных/слабых сторон,
  • базовых гиперпараметров и практических нюансов.

Ниже — структурированный обзор часто применяемых алгоритмов, с акцентом на практическое использование.

Линейные модели

  1. Линейная регрессия (Ordinary Least Squares)
  • Задача: регрессия (прогноз непрерывной величины).
  • Идея:
    • аппроксимируем целевое значение линейной комбинацией признаков.
  • Плюсы:
    • интерпретируемость (веса как вклад признаков),
    • быстрая,
    • базовый strong baseline.
  • Минусы:
    • линейность: плохо моделирует сложные нелинейные зависимости без инженерии признаков,
    • чувствительна к мультиколлинеарности и выбросам.
  • На практике:
    • почти всегда используем регуляризацию:
      • Ridge (L2), Lasso (L1), Elastic Net.
  1. Логистическая регрессия
  • Задача: бинарная и многоклассовая классификация.
  • Выход: вероятность класса через сигмоиду/softmax.
  • Плюсы:
    • интерпретируемость,
    • быстрая,
    • хорошо работает с нормированными признаками,
    • baseline для многих задач (кредитный скоринг, churn, fraud).
  • Минусы:
    • линейность границы решений,
    • требует инженерии признаков для сложных зависимостей.
  1. Линейные SVM (и kernel SVM)
  • Используются для классификации; линейные — для больших sparse задач (NLP, high-dimensional).
  • Kernel SVM:
    • мощные для небольших датасетов,
    • плохо масштабируются по данным.

Деревья решений и ансамбли

  1. Дерево решений (Decision Tree)
  • Задачи: классификация, регрессия.
  • Идея:
    • рекурсивное разбиение пространства признаков по правилам вида “feature <= threshold”.
  • Плюсы:
    • интерпретируемость (правила),
    • умеет работать с нелинейностями и взаимодействиями признаков без их явного задания.
  • Минусы:
    • склонно к переобучению,
    • нестабильно к небольшим изменениям данных.
  1. Случайный лес (Random Forest)
  • Ансамбль деревьев решений:
    • bootstrap выборки + случайный поднабор признаков на каждом сплите.
  • Плюсы:
    • устойчив к переобучению из-за усреднения,
    • хорошо работает “из коробки”,
    • умеет оценивать важность признаков,
    • часто сильный baseline для табличных данных.
  • Минусы:
    • менее интерпретируем, чем одиночное дерево,
    • медленнее на очень больших данных,
    • размер модели может быть большим.
  1. Градиентный бустинг (XGBoost, LightGBM, CatBoost)
  • Ансамбль деревьев, обучающихся последовательно:
    • каждое новое дерево исправляет ошибки предыдущих.
  • Современный стандарт для табличных данных.
  • Плюсы:
    • высокая точность,
    • гибкость (много гиперпараметров),
    • хорошо работает с разными типами признаков.
  • Минусы:
    • требует аккуратной настройки (learning_rate, глубина, регуляризация),
    • чувствителен к quality данных,
    • может переобучаться при неправильной конфигурации.
  • Практика:
    • XGBoost/LightGBM/ CatBoost — частый выбор для production ML по табличным данным;
    • CatBoost особенно удобен для категориальных признаков.

Инстанс-базированные методы

  1. KNN (k-ближайших соседей)
  • Задачи: классификация, регрессия.
  • Идея:
    • предсказание по ближайшим объектам в пространстве признаков.
  • Плюсы:
    • простота,
    • не требует обучения (ленивая модель).
  • Минусы:
    • медленный на предсказании для больших датасетов,
    • чувствителен к масштабу признаков и размерности,
    • плохо масштабируется в продакшене.
  • Часто используется как baseline или в обучающих задачах; реже — в боевых системах (если не применяются специальные структуры для поиска ближайших соседей).

Вероятностные модели

  1. Наивный Байес (Naive Bayes)
  • Часто для текстовой классификации (spam detection, sentiment).
  • Плюсы:
    • очень быстрый,
    • хорошо работает на sparse-векторах,
    • устойчив, когда признаки условно независимы.
  • Минусы:
    • “наивное” предположение независимости редко соблюдается,
    • точность ограничена, но как baseline — часто хорош.

Кластеризация

  1. K-means
  • Задача: кластеризация.
  • Идея:
    • минимизировать внутрикластерное расстояние до центров.
  • Плюсы:
    • простота, скорость,
    • полезен для сегментации, предварительного анализа.
  • Минусы:
    • предположение сферических кластеров,
    • чувствительность к масштабированию и инициализации,
    • надо заранее задавать k.

Другие: hierarchical clustering, DBSCAN и т.п. — для поиска сложных кластеров.

Нейросетевые модели (в общих чертах)

  1. Полносвязные сети (MLP)
  • Для табличных/обобщённых данных, но обычно уступают бустингу на классических табличных задачах.
  1. CNN
  • Для изображений, видео.
  1. RNN/LSTM/GRU / Transformer-based модели
  • Для последовательностей, текста, временных рядов.
  1. Современные трансформеры
  • NLP, CV, multi-modal — BERT, GPT-подобные, ViT и т.д.

Практическая зрелость ответа:

Хороший ответ должен показывать:

  • Умение подобрать алгоритм под задачу:
    • табличные данные → деревья, бустинг, линейные модели;
    • тексты → TF-IDF + логистическая регрессия / Linear SVM / трансформеры;
    • изображения → CNN/трансформеры;
    • временные ряды → специальные модели или buстинг с лаговыми признаками.
  • Понимание ограничений:
    • KNN — плохо для больших данных;
    • деревья/ансамбли — мало интерпретируемы, но мощные;
    • линейные — быстрые и стабильные, но требуют feature engineering.
  • Понимание важности:
    • нормализации/масштабирования (для KNN, линейных, SVM),
    • работы с дисбалансом классов (class weights, sampling),
    • корректного сплита и метрик.

Краткая формулировка для интервью:

  • Осмысленно перечислить: линейные модели (регрессия, логистическая), деревья, случайный лес, градиентный бустинг, KNN, наивный Байес, базовые нейросети.
  • Уметь коротко сказать, где какой алгоритм обычно применим и какие у него сильные/слабые стороны.
  • Подчеркнуть опыт хотя бы с:
    • линейными моделями,
    • деревьями/случайным лесом,
    • градиентным бустингом (XGBoost/LightGBM/CatBoost),
    • а также разумный выбор метрик и предобработки под каждый алгоритм.

Вопрос 22. Объясни своими словами принцип работы одного из известных алгоритмов (например, KNN или линейной регрессии).

Таймкод: 00:25:12

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

Правильный ответ:

Разберём два алгоритма — KNN и линейную регрессию — так, как это ожидается при глубоком понимании.

KNN (k-ближайших соседей)

Идея:

  • Это instance-based (ленивая) модель:
    • не строит явной параметрической функции на этапе обучения;
    • “обучение” — фактически запоминание обучающей выборки.
  • Для нового объекта:
    • считаем расстояние до всех обучающих примеров;
    • выбираем k ближайших;
    • в классификации:
      • голосование по меткам соседей (часто с возможным взвешиванием по расстоянию);
    • в регрессии:
      • усреднение значений целевой переменной соседей.

Ключевые моменты:

  • Выбор метрики:
    • Евклидово расстояние для непрерывных признаков;
    • Манхэттенское, косинусное, Hamming и т.д. в зависимости от природы признаков.
  • Масштабирование:
    • Обязательно нормализовать/стандартизовать признаки, иначе признаки с большим масштабом доминируют в расстоянии.
  • Параметр k:
    • Малый k → модель шумная, склонна к переобучению.
    • Большой k → сглаживание, риск недообучения и игнорирования локальной структуры.
  • Сложность:
    • Наивный предикт: O(N * d) на запрос (N — объектов, d — признаков).
    • Для больших данных нужны структуры ускоренного поиска (k-d tree, ball tree, ANN-индексы), но в очень высоких размерностях они деградируют.
  • Когда использовать:
    • Маленькие/средние датасеты,
    • Чёткое определение метрики близости,
    • Как baseline или для задач рекомендаций/поиска похожих объектов.

Пример (Python, sklearn):

from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline

model = make_pipeline(
StandardScaler(),
KNeighborsClassifier(n_neighbors=5, metric="euclidean")
)

model.fit(X_train, y_train)
y_pred = model.predict(X_test)

Линейная регрессия

Идея:

  • Модель предполагает линейную зависимость целевой переменной y от набора признаков x:

    y ≈ w0 + w1 * x1 + w2 * x2 + ... + wd * xd

  • Задача обучения:

    • подобрать вектор весов w, минимизирующий функцию потерь (обычно сумму квадратов ошибок).

Формально:

  • Ищем w, минимизирующий:

    sum_i (y_i - (w0 + w · x_i))^2

Способы решения:

  • Закрытая формула (Normal Equation) для небольших задач.
  • Градиентный спуск и его варианты — в реальных системах и при больших данных.

Расширения и практические моменты:

  1. Регуляризация:

    • Обычная линейная регрессия (OLS) может:
      • переобучаться,
      • страдать от мультиколлинеарности.
    • Используются:
      • Ridge (L2): добавляем λ * ||w||^2;
      • Lasso (L1): добавляем λ * ||w||_1 (даёт разреженные веса);
      • Elastic Net: комбинация L1 и L2.
    • Это:
      • стабилизирует модель,
      • уменьшает variance,
      • иногда помогает в отборе признаков.
  2. Масштабирование и признаки:

    • Для регуляризованных моделей нормализация важна (веса сравнимы).
    • Важен feature engineering:
      • полиномиальные признаки,
      • взаимодействия,
      • лог-преобразования и т.п.
    • Это позволяет линейной модели приближать нелинейные зависимости.
  3. Свойства:

    • Интерпретируемость:
      • знак и величина w_j — вклад признака;
      • удобно для бизнес-аналитики, скоринговых моделей.
    • Чувствительность к выбросам:
      • квадратичная ошибка сильно штрафует экстремальные точки;
      • можно использовать робастные варианты (Huber, quantile regression).

Пример (Python, sklearn):

from sklearn.linear_model import LinearRegression, Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline

# Базовая линейная регрессия
lr = LinearRegression()
lr.fit(X_train, y_train)

# Регуляризованная модель (Ridge)
ridge_model = make_pipeline(
StandardScaler(),
Ridge(alpha=1.0)
)
ridge_model.fit(X_train, y_train)

С точки зрения зрелого ответа:

  • Важно не только описать словами, но и показать понимание:
    • у KNN нет явной фазы обучения, предсказание дорогое, критично масштабирование и выбор метрики;
    • у линейной регрессии:
      • явно задаётся функциональная форма,
      • оптимизируется квадратичная ошибка,
      • регуляризация и обработка признаков — неотъемлемая часть практического использования.
  • Можно уверенно выбрать один алгоритм и объяснить его так, чтобы было видно понимание шагов, ограничений и реальных применений.

Вопрос 23. Что ты знаешь о нейронных сетях и какие типы сетей уместно использовать в разных задачах?

Таймкод: 00:26:22

Ответ собеседника: неполный. Упоминает полносвязные сети и RNN, знает про функции активации. RNN описывает частично верно (зависимость от прошлых состояний), но неуверенно, LSTM не знает. Практический опыт ограничен учебными примерами и использованием готовых моделей.

Правильный ответ:

Нейронные сети — это класс моделей, которые аппроксимируют сложные (часто сильно нелинейные) зависимости между входом и выходом за счёт композиции простых вычислительных блоков (нейронов) с обучаемыми параметрами (весами). Важны две вещи:

  • архитектура (как слои связаны);
  • алгоритм обучения (градиентный спуск / его варианты через backpropagation).

Ниже — системный обзор основных типов сетей и их практического применения.

Базовые элементы нейросетей:

  1. Линейный слой:

    • Вычисляет y = W x + b.
    • Без нелинейности вся сеть эквивалентна одной линейной модели.
  2. Функции активации:

    • Добавляют нелинейность, чтобы модель могла приближать сложные функции.
    • Типичные:
      • ReLU: max(0, x) — де-факто стандарт в современных архитектурах.
      • LeakyReLU, GELU, ELU — сглаженные/улучшенные варианты ReLU.
      • Sigmoid — исторически популярна, сейчас в скрытых слоях используется редко (vanishing gradients), актуальна в выходах для бинарной классификации.
      • Tanh — схожие проблемы, но иногда применима.
    • Правильный выбор активации критичен для стабильного обучения.
  3. Обучение: backpropagation + градиентный спуск:

    • Считаем loss (например, cross-entropy или MSE).
    • Вычисляем градиенты по всем параметрам.
    • Обновляем веса (SGD, Adam, RMSProp и т.п.).
    • Важны:
      • нормализация (BatchNorm/LayerNorm),
      • регуляризация (dropout, weight decay),
      • корректный learning rate.

Типы нейронных сетей и где они применяются:

  1. Полносвязные (Feedforward, MLP)
  • Архитектура:
    • несколько плотных слоев (Dense), каждый связан со всеми нейронами предыдущего.
  • Применения:
    • табличные данные,
    • простые регрессионные и классификационные задачи,
    • небольшие/структурированные признаки.
  • Плюсы:
    • универсальная аппроксимация (теорема универсальной аппроксимации),
    • простая реализация.
  • Минусы:
    • плохо масштабируются на высокоразмерные сложные структуры (картинки, длинные последовательности),
    • сильно подвержены переобучению на сыром сложном сигнале без хорошего feature engineering.
  1. Свёрточные сети (CNN)
  • Архитектура:
    • свёрточные слои, pooling, часто с skip-коннектами (ResNet и др.).
  • Применения:
    • компьютерное зрение:
      • классификация изображений,
      • детекция и сегментация объектов,
      • обработка медицинских снимков,
      • OCR и т.п.
  • Ключевые идеи:
    • локальные рецептивные поля (учёт локальной структуры),
    • shared weights (одни и те же фильтры по всему изображению),
    • инвариантность к сдвигам.
  • Практически:
    • современные CV-системы часто используют или CNN, или vision transformers, или их комбинации.
  1. Рекуррентные сети (RNN, LSTM, GRU)
  • Архитектура:
    • последовательно обрабатывают вход, передавая скрытое состояние h_t от шага к шагу.
  • Применения (классически):
    • временные ряды,
    • последовательности токенов (текст, события),
    • задачи генерации последовательностей.
  • Проблемы у vanilla RNN:
    • vanishing/exploding gradients,
    • плохо держат долгосрочные зависимости.
  • LSTM и GRU:
    • добавляют gating-механизмы (input/output/forget gates),
    • лучше запоминают долгосрочный контекст,
    • были стандартом для NLP и последовательностей до прихода трансформеров.
  • Сейчас:
    • в новых задачах чаще используются трансформеры, но понимание RNN/LSTM/GRU всё ещё важно.
  1. Трансформеры (Transformers)
  • Архитектура:
    • основана на механизме self-attention:
      • модель “смотрит” на все элементы последовательности одновременно и учится важным зависимостям.
  • Применения:
    • NLP: машинный перевод, summarization, QA, чат-боты, поиск.
    • CV: Vision Transformer.
    • Мультимодальные модели.
  • Практически:
    • современные большие языковые модели (включая те, что вы используете сейчас) — это трансформеры.
    • ключевой стандарт для сложных последовательных и текстовых задач.
  1. Автоэнкодеры, вариационные автоэнкодеры, GAN
  • Автоэнкодеры:
    • сжать вход в латентное представление и восстановить его обратно.
    • Применения:
      • уменьшение размерности,
      • аномалия детекция,
      • генерация.
  • VAE:
    • вероятностная формулировка, даёт гладкое латентное пространство.
  • GAN:
    • состязательные сети для генерации реалистичных данных (изображения, речь и т.п.).

Практические моменты, которые стоит уметь проговорить:

  • Выбор типа сети по задаче:

    • табличные данные:
      • чаще бустинг (XGBoost/LightGBM/CatBoost), MLP реже;
    • изображения:
      • CNN или vision transformers;
    • текст:
      • трансформеры (BERT, GPT-подобные, seq2seq),
      • для простых задач — TF-IDF + линейные модели;
    • временные ряды:
      • специализированные архитектуры, RNN/LSTM/GRU, 1D-CNN, трансформеры.
  • Регуляризация и устойчивость:

    • dropout,
    • weight decay (L2),
    • BatchNorm/LayerNorm,
    • ранняя остановка,
    • data augmentation.
  • Практический опыт:

    • использование фреймворков:
      • PyTorch, TensorFlow/Keras;
    • умение:
      • построить базовую архитектуру (MLP/CNN),
      • настроить лосс, оптимизатор, learning rate schedule,
      • контролировать overfitting (train vs val curves).

Краткая формулировка для интервью:

  • Объяснить, что:
    • полносвязные сети — базовый строительный блок;
    • CNN — стандарт для изображений;
    • RNN/LSTM/GRU — классический подход к последовательностям;
    • трансформеры — современный стандарт для текста и многих последовательных задач;
  • Уметь описать:
    • роль функций активации,
    • идею backpropagation,
    • базовые методы борьбы с переобучением и выбора архитектуры под задачу.

Вопрос 24. Что ты делал с диффузионными моделями и как понимаешь принцип их работы?

Таймкод: 00:30:30

Ответ собеседника: неполный. Использовал готовые диффузионные модели для генерации изображений по тексту; описывает в общих чертах: модель итеративно восстанавливает картинку из шума. Технические детали и ключевые понятия не раскрывает.

Правильный ответ:

Диффузионные модели — класс генеративных моделей, которые учатся обращать стохастический процесс "испорчивания" данных шумом. Интуитивно:

  • Прямой (forward) процесс: постепенно добавляем шум к реальным данным, пока они не превратятся в почти чистый шум.
  • Обратный (reverse) процесс: обученная модель шаг за шагом убирает шум, восстанавливая структуру данных — изображение, звук и т.д.
  • Для text-to-image: в обратном процессе модель не просто убирает шум, а делает это так, чтобы итоговое изображение соответствовало текстовому запросу (condition).

Это ключевая идея, но для уверенного технического ответа важно понимать архитектуру процесса и роль обучения.

Базовая схема работы (упрощенно)

  1. Forward diffusion (обучающий процесс "порчи" данных):
  • У нас есть реальные данные x₀ (например, изображения).

  • Определяем последовательность шагов t = 1..T, на каждом из которых добавляем небольшой гауссовский шум:

    x_t = sqrt(α_t) * x_{t-1} + sqrt(1 - α_t) * ε, где ε ~ N(0, I)

  • При достаточном количестве шагов T получаем x_T, распределение которого близко к N(0, I) — почти чистый шум.

  • Параметры {α_t} образуют "шедулер" шума (линейный, косинусный и т.п.).

  1. Обучение модели (reverse process):
  • В идеале хотим модель, которая умеет восстанавливать p(x_{t-1} | x_t), то есть один шаг "обратной" диффузии.
  • На практике:
    • обучается нейросеть ε_θ(x_t, t, condition), которая по зашумлённому x_t, шагу t (и опциональному условию, например тексту) предсказывает добавленный шум ε.
  • Лосс:
    • чаще всего MSE между истинным шумом и предсказанным: L = E[||ε - ε_θ(x_t, t, condition)||²]
    • это упрощает обучение и хорошо работает на практике.
  1. Генерация (sampling):

Процесс генерации — это имитация обратного процесса:

  • Начинаем с x_T ~ N(0, I) — случайный шум.
  • Для t = T..1:
    • подаём (x_t, t, condition) в модель ε_θ;
    • вычисляем оценку "очищенного" x_{t-1} по формуле из диффузионного процесса (учитывая α_t и предсказанный шум);
    • при необходимости добавляем стохастический компонент.
  • После серии шагов получаем x_0 — сгенерированное изображение.

В text-to-image моделях:

  • condition — это текстовое описание:
    • текст кодируется encoder-ом (например, CLIP text encoder);
    • эмбеддинг текста подается в диффузионную сеть (U-Net) через:
      • cross-attention,
      • или конкатенацию/адаптивные слои.
  • В результате модель учится:
    • при обратной диффузии "формировать" структуру шума так, чтобы итоговое изображение соответствовало семантике текста.

Ключевые компоненты современных диффузионных моделей:

  1. Архитектура сети:
  • Как правило, U-Net:
    • downsampling → bottleneck → upsampling,
    • skip-коннекты для сохранения деталей.
  • Добавляются:
    • positional/time embeddings (кодирование шага t),
    • механизмы condition (text/image/label).
  1. Типы диффузионных моделей:
  • DDPM (Denoising Diffusion Probabilistic Models):
    • базовая формулировка, вероятностный процесс.
  • DDIM:
    • детерминистический sampling, ускоряет генерацию.
  • Latent diffusion (как в Stable Diffusion):
    • диффузия происходит не в пространстве пикселей, а в латентном пространстве autoencoder-а:
      • encoder: сжимает изображение в латент;
      • U-Net-диффузия: работает в компактном пространстве;
      • decoder: разворачивает латент обратно в изображение.
    • Это сильно ускоряет обучение и генерацию.
  1. Контроль и условности (conditioning):
  • Text-to-image:
    • CLIP/текстовый encoder → эмбеддинг → cross-attention в U-Net.
  • Class-conditioned:
    • условие — one-hot/class embedding.
  • Image-conditioned:
    • inpainting, super-resolution, style transfer:
      • используем часть изображения или низкое разрешение как condition.
  1. Практические моменты и почему это важно понимать:
  • Преимущества диффузионных моделей:

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

    • Многократные итерации sampling → высокая стоимость генерации.
    • Это частично решается:
      • улучшенными schedulers (DDIM, DPM-Solver),
      • уменьшением числа шагов (10–50 вместо сотен/тысяч).
  • Инженерные аспекты:

    • high-load inference:
      • оптимизация U-Net (mixed precision, quantization),
      • батчинг запросов,
      • кэширование текстовых эмбеддингов.

Краткая формулировка для интервью:

  • Диффузионная модель учится обращать процесс добавления шума:
    • на обучении: к реальным данным по шагам добавляется шум, сеть учится по зашумлённым данным предсказывать шум (или "очищенную" версию);
    • на генерации: начинаем с шума и многократно применяем обученную модель удаления шума, получая реалистичный образ.
  • В text-to-image моделях:
    • обратный процесс дополнительно кондиционируется текстом, чтобы результат соответствовал описанию.
  • Важно:
    • понимать forward/reverse процессы,
    • роль U-Net и conditioning,
    • отличие latent diffusion (Stable Diffusion) от пиксельных моделей.

Вопрос 25. Для чего используются NumPy и pandas, и в чём различия их назначения?

Таймкод: 00:32:29

Ответ собеседника: правильный. Корректно пояснил, что NumPy используется для эффективной работы с числами и массивами/матрицами (под капотом C, база для многих ML-библиотек), а pandas — для работы с табличными данными, выборок и анализа, часто вместе с визуализацией.

Правильный ответ:

NumPy и pandas решают разные, но взаимосвязанные задачи в Python-экосистеме для научных вычислений и анализа данных.

NumPy:

Основное назначение:

  • Эффективные численные вычисления:
    • многомерные массивы (ndarray),
    • векторизованные операции,
    • линейная алгебра, работа с матрицами,
    • генерация случайных чисел,
    • базовые численные алгоритмы.

Ключевые особенности:

  • Реализован на C/Fortran, работает значительно быстрее чистых Python-циклов.
  • Операции над массивами выполняются пакетно (vectorized), минимизируя Python overhead.
  • Широко используется как "низкоуровневая" основа:
    • для pandas,
    • для scikit-learn,
    • для многих DL-фреймворков (исторически),
    • для численных/научных библиотек.

Пример (NumPy как матричная арифметика):

import numpy as np

a = np.array([[1, 2], [3, 4]])
b = np.array([[10, 20], [30, 40]])

# Поэлементные операции
c = a + b # [[11, 22], [33, 44]]

# Матричное умножение
d = a @ b # [[70, 100],
# [150, 220]]

Когда использовать NumPy:

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

pandas:

Основное назначение:

  • Анализ и обработка табличных и временных данных:
    • DataFrame (таблица: строки-наблюдения, столбцы-признаки),
    • Series (одномерный labeled-вектор),
    • индексация, фильтрация, groupby-агрегации, join/merge, ресемплинг.

Ключевые особенности:

  • Структуры с метаданными:
    • осмысленные имена столбцов и индексов;
    • удобные выборки по меткам, а не только по позициям.
  • Высокоуровневые операции:
    • groupby / aggregation,
    • join/merge,
    • pivot / pivot_table,
    • работа с датами и временными рядами,
    • чтение/запись CSV, Parquet, SQL и т.д.
  • Внутри активно использует NumPy для хранения данных, но даёт более богатый API для "data wrangling".

Пример (pandas как табличный инструмент):

import pandas as pd

df = pd.DataFrame({
"user_id": [1, 2, 3, 4],
"city": ["Moscow", "SPB", "Moscow", "Kazan"],
"amount": [100, 200, 150, 50],
})

# Фильтрация
df_moscow = df[df["city"] == "Moscow"]

# Группировка и агрегация
stats = df.groupby("city")["amount"].sum()
# city
# Kazan 50
# Moscow 250
# SPB 200

# Join с другой таблицей (как в SQL)
cities = pd.DataFrame({
"city": ["Moscow", "SPB", "Kazan"],
"region": ["RU-MOW", "RU-SPE", "RU-TA"]
})

df_merged = df.merge(cities, on="city", how="left")

Когда использовать pandas:

  • Если задача:
    • загрузить датасет (CSV, SQL, Parquet),
    • очистить данные (NaN, дубликаты, типы),
    • трансформировать признаки,
    • агрегировать/джойнить по ключам,
    • подготовить данные для обучения моделей (обычно → NumPy или прямо в ML-библиотеки).

Связь и различия:

  • NumPy:

    • про численные массивы, скорость и математику;
    • индексы — позиционные, без семантики колонок;
    • удобен для матричных операций, линейной алгебры и внутренних реализаций.
  • pandas:

    • про табличные, бизнес-ориентированные данные;
    • имеет имена столбцов, индексы, мощный API для трансформаций;
    • строится поверх NumPy.

Простая практическая линия:

  • "pandas для подготовки данных" → "NumPy / массивы / тензоры для модели".

Краткая формулировка для интервью:

  • NumPy — базовая библиотека для высокопроизводительных численных вычислений и работы с многомерными массивами.
  • pandas — высокоуровневая библиотека для анализа и трансформации табличных данных, построена поверх NumPy и предоставляет DataFrame/Series и удобный API для работы с реальными датасетами.

Вопрос 26. Для чего используется pandas и чем он удобнее обычных структур при работе с табличными данными?

Таймкод: 00:33:41

Ответ собеседника: правильный. Отмечает, что pandas позволяет удобно работать с табличными данными и столбцами, проще искать зависимости и визуально воспринимать данные, чем с “сырыми” массивами/матрицами.

Правильный ответ:

pandas — это специализированная библиотека для работы с табличными и временными данными, которая существенно упрощает анализ, очистку и трансформацию данных по сравнению с использованием базовых структур Python (list, dict) или "голого" NumPy.

Ключевые преимущества pandas при работе с табличными данными:

  1. Семантика табличных данных
  • Основные структуры:
    • DataFrame — аналог таблицы: строки = объекты/наблюдения, столбцы = признаки.
    • Series — одномерный labeled-вектор (столбец или индекс).
  • В отличие от обычных списков и NumPy-массивов:
    • у столбцов есть имена;
    • у строк есть индексы (которые могут не быть просто 0..N-1);
    • операции можно выражать через имена колонок, а не через "магические" индексы.

Пример:

import pandas as pd

df = pd.DataFrame({
"user_id": [1, 2, 3],
"city": ["Moscow", "SPB", "Kazan"],
"amount": [100, 200, 150],
})

# Понятно по смыслу:
high = df[df["amount"] > 150]
  1. Удобная фильтрация, агрегации и группировки

pandas даёт декларативный, компактный и читаемый API для операций, которые на list/dict/чистом NumPy писались бы вручную циклами:

  • Фильтрация:

    df_moscow = df[df["city"] == "Moscow"]
  • Группировка и агрегация:

    total_by_city = df.groupby("city")["amount"].sum()
  • Сводные таблицы:

    pivot = df.pivot_table(values="amount", index="city", aggfunc="mean")
  1. Join/merge как в SQL

pandas позволяет естественно объединять таблицы по ключам:

cities = pd.DataFrame({
"city": ["Moscow", "SPB"],
"region": ["RU-MOW", "RU-SPE"],
})

df_merged = df.merge(cities, on="city", how="left")

Это намного проще и безопаснее, чем ручная синхронизация нескольких списков или словарей.

  1. Работа с пропусками и типами данных
  • Пропуски:
    • isna(), fillna(), dropna() — компактные и выразительные операции для обработки NaN.
  • Типы:
    • удобное преобразование типов (astype),
    • специализированная поддержка дат/времени (datetime64, ресемплинг, time-based индекс).

Пример:

df["amount"] = df["amount"].fillna(0)
df["date"] = pd.to_datetime(df["date"])
  1. Интеграция с NumPy, matplotlib и ML-библиотеками
  • DataFrame легко конвертируется в NumPy-массивы:
    X = df[["amount"]].to_numpy()
  • Хорошо работает с визуализацией:
    df["amount"].hist()
  • Является стандартным входом/выходом для многих ML-инструментов (scikit-learn, statsmodels и др.).
  1. Производительность и лаконичность
  • Под капотом pandas опирается на NumPy и векторизованные операции:
    • множество операций реализовано на C/компилируемых частях;
    • избавляет от Python-циклов при обработке больших объемов данных.
  • То, что в чистом Python потребовало бы десятки строк циклов и условий, в pandas пишется в 1–3 строки и легче читается и ревьюится.
  1. Читаемость и сопровождение кода
  • Код на pandas ближе к SQL/аналитическому стилю:
    • проще анализировать, дебажить, обсуждать в команде аналитиков и разработчиков.
  • По сравнению с наборами list/dict:
    • меньше шансов сделать ошибку в индексах,
    • проще рефакторить (имеются имена колонок и явные операции).

Краткая суть:

  • pandas используется для:
    • загрузки, очистки, трансформации, агрегации и анализа табличных данных.
  • Он удобнее обычных структур и "чистого" NumPy тем, что:
    • предоставляет осмысленные табличные абстракции (DataFrame/Series),
    • имеет мощный высокоуровневый API (groupby, merge, pivot, resample),
    • интегрируется с остальной экосистемой анализа данных,
    • делает код короче, понятнее и менее ошибкоопасным.

Вопрос 27. Расскажи о своём опыте работы в прошлой компании.

Таймкод: 00:34:28

Ответ собеседника: правильный. Кратко описал опыт поддержки ERP-системы Microsoft в ритейле: исправление багов, небольшие фичи, выполнение запросов бизнеса, активная работа с базой данных; отметил, что опыт частично нерелевантен ML, но даёт практику промышленной разработки.

Правильный ответ:

При ответе на такой вопрос важно структурированно показать:

  • реальный боевой опыт,
  • работу с продакшн-системами,
  • ответственность,
  • навыки, которые транслируются в текущую позицию (разработка, качество, работа с данными, взаимодействие с бизнесом),
  • а не пересказывать резюме в общем виде.

Хороший развёрнутый ответ мог бы выглядеть так.

  1. Контекст проекта

Опиши систему кратко, но по-деловому:

  • Что это за ERP/информационная система.
  • Масштаб: количество пользователей, точек, интеграций.
  • Критичность: деньги, склады, заказы, логистика.

Например:

  • Это централизованная ERP-система крупной розничной сети, завязанная на:
    • управление заказами,
    • остатками на складах и в магазинах,
    • ценами и акциями,
    • интеграции с кассами, бухгалтерией, логистикой.
  1. Зона ответственности

Подчеркни, что занимался не “абстрактным кодом”, а конкретными задачами с влиянием на бизнес.

Типичные направления (можно адаптировать под факт):

  • Поддержка и развитие ядра ERP:

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

    • доработки отчётов,
    • новые поля/атрибуты в справочниках,
    • дополнительные проверки и валидации,
    • настройка бизнес-правил.
  • Интеграции:

    • взаимодействие с внешними системами:
      • выгрузки/загрузки данных,
      • обмен через файлы, API, очереди.
    • обеспечение надёжности и идемпотентности таких процессов.
  1. Работа с базой данных

Сделай акцент, что это не просто "писал SELECT", а системная работа с данными:

  • Написание и оптимизация SQL-запросов:
    • сложные join-ы,
    • агрегации для отчётности,
    • выборки для аналитики и поддержки бизнеса.
  • Разбор проблем производительности:
    • поиск медленных запросов,
    • анализ планов выполнения,
    • индексы, денормализация, корректировка запросов.
  • Обеспечение целостности:
    • транзакции,
    • ограничения,
    • аккуратные миграции и правки данных в проде.

Пример SQL-фрагмента (концептуально):

SELECT s.store_id,
p.sku,
SUM(m.quantity) AS total_stock
FROM stock_movements m
JOIN products p ON p.id = m.product_id
JOIN stores s ON s.id = m.store_id
WHERE m.movement_date >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY s.store_id, p.sku;
  1. Качество, процессы и инженерная зрелость

Важно показать, что ты привык работать как инженер, а не как "техподдержка":

  • Code review:
    • участие в ревью изменений,
    • соблюдение coding style и внутренних стандартов.
  • Тестирование:
    • регрессионные проверки при изменении критичных модулей,
    • написание или хотя бы запуск тестов (юнит/интеграционные, если были).
  • Работа с инцидентами:
    • разбор продакшн-проблем,
    • поиск корневых причин (root cause analysis),
    • фиксы без нарушения работы системы.
  • CI/CD:
    • участие в процессе релизов,
    • понимание, как изменения проходят путь до продакшена.
  1. Взаимодействие с бизнесом

Подчеркни опыт коммуникаций:

  • Работа с требованиями:
    • получение задач от бизнеса (аналитики, операционный блок),
    • уточнение требований,
    • объяснение ограничений и последствий.
  • Приоритизация:
    • умение балансировать между “починить срочно” и “сделать правильно”.
  • Понимание домена:
    • логистика, скидки, остатки, отчётность — это полезный опыт для любых data-driven систем.
  1. Связь с дальнейшим развитием (в сторону разработки и ML/данных)

Даже если стек был не ML и не Go, важно показать трансфер навыков:

  • Работа с реальными, “грязными” данными:
    • понимание, что данные в продакшене:
      • неполные,
      • противоречивые,
      • требуют валидации и очистки.
  • Ответственность за стабильность:
    • изменения нельзя вносить "на удачу";
    • нужна внимательность, откатные сценарии, тесты.
  • Опыт в оптимизации:
    • и по коду, и по базе.
  • Умение входить в чужой легаси-код:
    • читать,
    • разбираться,
    • аккуратно расширять.

Краткая, сильная версия ответа для интервью:

  • Кратко описать контекст ERP-системы и масштабы.
  • Чётко перечислить:
    • поддержка продакшн-функционала (багфиксы, мелкие фичи),
    • много SQL и работа с производительностью,
    • интеграции и отчёты,
    • взаимодействие с бизнес-заказчиками.
  • Сделать акцент: этот опыт научил аккуратной промышленной разработке, работе с критичными данными и продакшн-системами, что напрямую переносится в любую инженерную роль, включая разработку backend/ML-сервисов.

Вопрос 28. Как у тебя обстоят дела с SQL и какие задачи ты выполнял?

Таймкод: 00:35:14

Ответ собеседника: правильный. Уверенно говорит, что хорошо владеет SQL, часто писал сложные запросы в контексте ERP-системы; базы данных сам не администрировал, но имеет существенный практический опыт написания запросов.

Правильный ответ:

Сильный ответ по SQL должен показать не только умение писать базовые SELECT’ы, но и системное владение:

  • реляционной моделью,
  • сложными запросами,
  • оптимизацией,
  • транзакционной семантикой,
  • пониманием, как это встраивается в приложения (в т.ч. на Go).

Ключевые аспекты, которые стоит отражать.

  1. Типы задач, которые ожидаемо решать уверенно
  • Получение отчетов и агрегатов:
    • GROUP BY, оконные функции, вложенные запросы.
  • Поиск и анализ проблемных данных:
    • неконсистентность, дубликаты, нарушения ограничений.
  • Сложные выборки:
    • множественные JOIN (INNER/LEFT/RIGHT/FULL),
    • комбинирование условий,
    • подзапросы в WHERE/FROM/SELECT.
  • Подготовка данных для других систем:
    • выгрузки, представления (VIEW), временные таблицы.

Пример комплексного запроса:

SELECT
o.customer_id,
COUNT(*) AS orders_count,
SUM(o.amount) AS total_amount,
MAX(o.created_at) AS last_order_at
FROM orders o
JOIN customers c
ON c.id = o.customer_id
WHERE
o.status = 'completed'
AND o.created_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY
o.customer_id
HAVING
SUM(o.amount) > 10000
ORDER BY
total_amount DESC
LIMIT 100;

Такие запросы типичны для ERP/BI-кейсов и демонстрируют владение базовой аналитикой.

  1. Глубина владения: JOIN’ы, агрегаты, оконные функции
  • JOIN:
    • понимание, когда использовать INNER vs LEFT JOIN;
    • умение писать запросы без дубликатов и "взрывов" данных.
  • Агрегации:
    • GROUP BY, HAVING, агрегационные функции (SUM, COUNT, AVG, MAX, MIN).
  • Оконные функции (важный маркер уровня):
    • ROW_NUMBER(), RANK(), DENSE_RANK(),
    • оконные агрегаты SUM() OVER (...), AVG() OVER (...),
    • разделение по партциям (PARTITION BY).

Пример оконной функции:

SELECT
o.customer_id,
o.id AS order_id,
o.amount,
SUM(o.amount) OVER (
PARTITION BY o.customer_id
ORDER BY o.created_at
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM orders o
WHERE o.created_at >= CURRENT_DATE - INTERVAL '30 days';
  1. Целостность данных и транзакции

Ожидается понимание:

  • Первичные и внешние ключи, уникальные и check-ограничения.
  • Транзакции:
    • BEGIN / COMMIT / ROLLBACK;
    • зачем нужны;
    • что такое атомарность, согласованность, изолированность, долговечность (ACID).
  • Уровни изоляции:
    • хотя бы на концептуальном уровне:
      • грязное чтение, non-repeatable read, phantom read;
      • как СУБД балансирует между изоляцией и конкурентностью.
  1. Оптимизация запросов

Зрелый опыт включает:

  • Понимание индексов:
    • B-tree индексы, составные индексы, выбор правильного порядка полей;
    • влияние индексов на фильтрацию и сортировку.
  • Анализ планов выполнения:
    • EXPLAIN / EXPLAIN ANALYZE:
      • Nested Loop, Hash Join, Seq Scan, Index Scan.
  • Типичные практики:
    • избегать "SELECT *" в тяжёлых запросах;
    • фильтровать как можно раньше;
    • не злоупотреблять подзапросами, когда JOIN читаемее и быстрее.

Пример использования индекса в прикладном коде (Go):

type Order struct {
ID int64
CustomerID int64
Amount float64
CreatedAt time.Time
}

const findOrdersByCustomerSQL = `
SELECT id, customer_id, amount, created_at
FROM orders
WHERE customer_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
`

func (r *OrderRepo) FindByCustomer(
ctx context.Context,
customerID int64,
limit, offset int,
) ([]Order, error) {
rows, err := r.db.QueryContext(ctx, findOrdersByCustomerSQL, customerID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()

var res []Order
for rows.Next() {
var o Order
if err := rows.Scan(&o.ID, &o.CustomerID, &o.Amount, &o.CreatedAt); err != nil {
return nil, err
}
res = append(res, o)
}
return res, rows.Err()
}

Под это место обычно создаётся индекс:

CREATE INDEX idx_orders_customer_created_at
ON orders (customer_id, created_at DESC);
  1. Встраивание SQL в приложения

Для прикладного разработчика важно:

  • Понимать, как писать SQL так, чтобы:
    • он был безопасен (prepared statements, защита от SQL injection),
    • предсказуем по производительности,
    • читабелен для команды.
  • Знать подходы:
    • миграции схемы (Liquibase, Flyway, goose и т.п.),
    • версионирование схемы,
    • разделение ответственности: бизнес-логика в коде, а не в "магии" триггеров.
  1. Как это презентовать на интервью

Сильный ответ:

  • Подтвердить уверенное владение SQL.
  • Кратко привести реальные типы задач:
    • сложные JOIN/агрегации для отчётности и аналитики;
    • выборки для бизнес-отчётов, сверок, мониторинга;
    • диагностика проблем в производственных данных запросами;
    • оптимизация тяжёлых запросов.
  • Показать понимание:
    • индексов,
    • транзакций,
    • базовой оптимизации.
  • Честно обозначить границу:
    • администрирование (backup, репликация, тюнинг параметров СУБД) — отдельная роль,
    • но чтение планов, проектирование схем и индексов — знакомо и практикуется.

Вопрос 29. Что ты знаешь о SQLAlchemy и как понимаешь её назначение?

Таймкод: 00:36:18

Ответ собеседника: неполный. Знает SQLAlchemy на теории, практически почти не использовал. Описывает как инструмент для динамического формирования запросов и более удобной записи SQL в стиле Python-кода, абстрагирующий сырые SQL-строки, без раскрытия архитектуры и ключевых концепций.

Правильный ответ:

SQLAlchemy — это зрелый и мощный стек для работы с реляционными базами данных в Python, который предоставляет:

  • гибкий, выразительный Core-уровень (конструктор SQL),
  • ORM-уровень для объектного маппинга,
  • унифицированный интерфейс к разным СУБД,
  • управление соединениями, транзакциями и жизненным циклом сессий.

Важно понимать, что SQLAlchemy — это не просто “обёртка над SQL”, а инструмент, который позволяет:

  • писать сложные запросы декларативно, не теряя контроля над SQL;
  • поддерживать крупные кодовые базы с чистой архитектурой доступа к данным;
  • избежать жёсткой привязки к конкретной СУБД (PostgreSQL, MySQL, SQLite и т.д.).

Ключевые концепции SQLAlchemy:

  1. Engine и Connection
  • Engine:
    • основной объект, инкапсулирующий:
      • пул соединений,
      • диалект СУБД (Postgres, MySQL и т.д.),
      • низкоуровневые детали подключения.
  • Создание:
from sqlalchemy import create_engine

engine = create_engine(
"postgresql+psycopg2://user:password@localhost:5432/mydb",
echo=False, # логировать SQL
pool_pre_ping=True, # проверка соединений
)
  • Через engine выполняются запросы, создаются сессии ORM.
  1. SQLAlchemy Core

Это слой, дающий декларативный конструктор SQL, сохраняя близость к “ручному” SQL:

  • Определение таблиц:
from sqlalchemy import MetaData, Table, Column, Integer, String

metadata = MetaData()

users = Table(
"users", metadata,
Column("id", Integer, primary_key=True),
Column("name", String, nullable=False),
Column("email", String, unique=True, nullable=False),
)
  • Конструирование запросов:
from sqlalchemy import select

stmt = select(users).where(users.c.email == "john@example.com")

with engine.connect() as conn:
row = conn.execute(stmt).fetchone()

Преимущества Core:

  • Статически описываем структуру и запросы,
  • Получаем безопасное формирование SQL без конкатенации строк,
  • Легко читать и дебажить,
  • Можно использовать как “тонкий слой” поверх реального SQL без тяжёлой ORM.
  1. ORM (Object-Relational Mapping)

ORM-слой SQLAlchemy позволяет маппить строки таблиц на Python-объекты:

  • Декларативное описание модели:
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker

class Base(DeclarativeBase):
pass

class User(Base):
__tablename__ = "users"

id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[str]

# Инициализация
engine = create_engine("postgresql+psycopg2://user:pass@localhost/mydb")
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
  • Работа через ORM:
def get_user_by_email(db, email: str) -> User | None:
return db.query(User).filter(User.email == email).first()

def create_user(db, name: str, email: str) -> User:
user = User(name=name, email=email)
db.add(user)
db.commit()
db.refresh(user)
return user

with SessionLocal() as db:
u = create_user(db, "John", "john@example.com")

Преимущества ORM:

  • Менее шаблонный код при типичных CRUD-операциях.
  • Единые модели, которые:
    • используются в бизнес-логике,
    • отражают схему БД.
  • Возможность декларативно задавать связи:
    • one-to-many, many-to-many, lazy/eager loading и т.д.

Важный момент: ORM SQLAlchemy не прячет SQL — при необходимости всегда можно спуститься на уровень Core или сырого SQL.

  1. Управление транзакциями и сессиями
  • Сессия (Session):
    • отвечает за:
      • транзакции,
      • identity map (каждая строка БД → один объект в сессии),
      • отслеживание изменений и их flush/commit.
  • Зрелое использование:
    • явный контроль границ сессии и транзакции (per-request / unit of work),
    • корректное закрытие сессий,
    • отсутствие "висящих" долгоживущих сессий в веб-приложениях.

Типичный паттерн (упрощённо, в духе unit-of-work):

from contextlib import contextmanager

@contextmanager
def get_session():
session = SessionLocal()
try:
yield session
session.commit()
except:
session.rollback()
raise
finally:
session.close()
  1. Безопасность и переносимость
  • SQLAlchemy заботится о:
    • безопасной подстановке параметров (подготовленные выражения) — защита от SQL injection при корректном использовании.
    • абстракции диалектов: минимизируем vendor lock-in, если не упираемся в специфичные фичи.
  • При этом:
    • можно воспользоваться возможностями конкретной БД (Postgres, MySQL) через диалект-специфичные расширения и сырой SQL.
  1. Где SQLAlchemy особенно уместна
  • Средние и крупные проекты на Python:
    • веб-сервисы (FastAPI, Flask),
    • backend-части ML-систем (хранение featurized данных, отслеживание экспериментов),
    • микросервисы поверх Postgres/MySQL.
  • Когда:
    • нужен контролируемый слой доступа к данным,
    • много сущностей и связей,
    • важно разделить доменную модель и уровень SQL,
    • нужна поддерживаемость и тестируемость.
  1. Аналогия с подходами в Go

Хотя вопрос про Python, полезно мыслить по-архитектурному:

  • В Go типичный стек:
    • database/sql + драйвер + легковесный слой (sqlx, squirrel, gorm).
  • SQLAlchemy ближе по идеологии к:
    • gorm (ORM) + squirrel (builder) одновременно:
      • есть и мощный ORM,
      • и низкоуровневый конструктор запросов.

Зрелый ответ в собеседовании:

  • Показать понимание, что SQLAlchemy:
    • включает два уровня: Core и ORM;
    • управляет соединениями и транзакциями;
    • абстрагирует SQL, но не скрывает его.
  • Упомянуть:
    • Engine, Session, декларативные модели;
    • преимущества: безопасность, выразительность, переносимость, тестируемость.
  • Отметить, что при необходимости можно:
    • комбинировать ORM и “сырые” запросы,
    • держать контроль над перформансом и сложными запросами.

Вопрос 30. Какой у тебя опыт работы с Matplotlib и для чего его имеет смысл использовать?

Таймкод: 00:37:27

Ответ собеседника: правильный. Использовал Matplotlib для анализа датасетов и построения графиков, отмечает, что визуализация помогает лучше понимать данные, чем просто смотреть на числа в pandas.

Правильный ответ:

Matplotlib — это базовая, низкоуровневая библиотека для визуализации данных в Python. Её роль — дать гибкий и контролируемый способ строить графики, поверх которого уже строятся более высокоуровневые библиотеки (seaborn, pandas.plot, многие dashboard-фреймворки).

Ключевые сценарии, в которых Matplotlib особенно уместен:

  1. Разведочный анализ данных (EDA)
  • Быстрый визуальный просмотр распределений и зависимостей:
    • понимание масштаба значений;
    • обнаружение выбросов;
    • интуитивное сравнение групп, классов, временных периодов.
  • Примеры:
    • распределение целевой переменной;
    • зависимость признаков от времени;
    • сравнение классов в задаче классификации.
import matplotlib.pyplot as plt

# Гистограмма распределения признака
plt.hist(df["amount"], bins=50)
plt.xlabel("Amount")
plt.ylabel("Count")
plt.title("Distribution of Amount")
plt.show()
  1. Связка с pandas и NumPy
  • Matplotlib хорошо интегрируется:
    • pandas.DataFrame.plot под капотом использует Matplotlib.
  • Типичный паттерн:
    • агрегации и подготовка данных в pandas;
    • визуализация через Matplotlib для тонкой настройки.
by_city = df.groupby("city")["amount"].sum()

plt.figure(figsize=(8, 4))
by_city.plot(kind="bar")
plt.ylabel("Total amount")
plt.title("Sales by city")
plt.tight_layout()
plt.show()
  1. Гибкость и кастомизация

Matplotlib позволяет детально контролировать:

  • подписи осей, шрифт, цветовую схему;
  • легенды, сетку, лимиты осей;
  • несколько осей/подграфиков (subplots);
  • стили и темы (plt.style.use).

Пример с несколькими графиками:

fig, ax = plt.subplots(1, 2, figsize=(10, 4))

ax[0].hist(df["feature1"], bins=30, color="steelblue")
ax[0].set_title("Feature 1")

ax[1].hist(df["feature2"], bins=30, color="orange")
ax[1].set_title("Feature 2")

plt.tight_layout()
plt.show()
  1. Использование в ML/DS-проектах

Практически полезные паттерны:

  • Отрисовка:
    • обучающих и валидационных метрик по эпохам (loss, accuracy, F1);
    • ROC-кривых, PR-кривых;
    • важности признаков (feature importance) для бустинга/лесов;
    • предсказаний vs реальных значений.
  • Диагностика:
    • learning curves;
    • распределение ошибок;
    • визуализация результатов кластеризации.

Пример: график обучения модели:

epochs = range(len(train_loss))
plt.plot(epochs, train_loss, label="train_loss")
plt.plot(epochs, val_loss, label="val_loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.title("Training dynamics")
plt.show()
  1. Почему важно уметь работать с Matplotlib, даже при наличии "удобных обёрток"
  • Высокоуровневые библиотеки (seaborn, plotly, pandas.plot):
    • дают быстрый результат,
    • но при сложных кастомизациях всё равно приходится опускаться до Matplotlib.
  • В продакшн-инструментах (отчёты, internal dashboards, генерируемая аналитика):
    • Matplotlib даёт контроль и воспроизводимость.
  • Важно:
    • понимать модель фигур/осей (Figure/Axes),
    • уметь управлять layout (subplots, tight_layout),
    • работать с легендами, аннотациями, форматированием.

Краткая формулировка для интервью:

  • Matplotlib — стандартный инструмент для визуализации в Python:
    • используется для разведочного анализа данных,
    • диагностики моделей,
    • построения настраиваемых графиков.
  • Его сила:
    • низкоуровневый контроль и гибкость,
    • тесная интеграция с NumPy/pandas,
    • возможность делать как быстрые черновые графики, так и аккуратные отчётные визуализации.