Защита
Рассказ на защите
Сценарий, чтобы пройтись по лабе по порядку и уверенно. Листаешь ноутбук сверху вниз, на каждом шаге смотришь в указанную ячейку и говоришь 2-4 фразы. Целиком выходит минут на 7-8.
- В Colab: «Среда выполнения → Перезапустить сеанс и выполнить все». Номера ячеек ниже даны для такого запуска: [1], [2], [3]...
- Открой ноутбук на первой ячейке, а этот сценарий на телефоне рядом.
- Говори своими словами, текст ниже нужен как опора. Числа лучше назвать точно, они показывают, что ты смотрел на результат.
Начало
Здравствуйте, могу рассказать по порядку? Лаба про анализ и предобработку данных о покупателях магазина: pandas и SQL, разведочный анализ, восстановление пропусков через kNN, кластеризация готовым kMeans и свой kMeans.
Раздел 1 · pandas и SQL
[2] · people = pd.read_csv('people.csv', sep=';')
Сначала прочитал три таблицы. Подвох в том, что у файлов разные разделители: точка с запятой, табуляция и запятая. Без правильного sep таблица читается одним столбцом. Во всех таблицах по 2240 клиентов, пропуски только в доходе, 24 штуки. Ещё сразу добавил import seaborn: ниже он используется, а в исходном ноутбуке его не было.
Если спросят: как связаны таблицы?
Через id клиента: у одного человека один id во всех трёх таблицах.
[4] и [5] · min_income_with_kids = ... / SELECT ABS(
Первое задание: минимальный доход среди тех, у кого kidhome больше нуля, это 2447, максимальный среди тех, у кого ноль, это 160803. Разница по модулю 158356. В pandas беру строки по маске через loc, в SQL делаю два подзапроса с MIN и MAX. Ответы совпали.
[6] и [7] · sweet_ids = ... / CASE WHEN id IN
Второе задание: доли PhD и остальных среди тех, кто покупает сладкое, то есть тратил на него больше нуля, и кто не покупает. Join использовать нельзя, поэтому в pandas беру список id сладкоежек и проверяю через isin, в SQL через подзапрос IN. Доли считаю через crosstab с normalize по строке, а в SQL как среднее от нулей и единиц. Получилось 0.18 PhD у сладкоежек и 0.37 у остальных, строки в сумме дают единицу.
Если спросят: почему AVG даёт долю?
PhD = 1, остальные = 0. Среднее это сумма единиц, делённая на размер группы, то есть доля PhD.
Раздел 2 · EDA
Markdown перед [8] · список полезных и бесполезных графиков
Из графиков Colab бесполезны все, где есть id: это номер, а не величина. Ещё бесполезны линии по номеру строки: порядок строк ничего не значит. Полезны гистограммы года рождения и дохода, на них видны выбросы, например доход 666666. Столбики по образованию и семейному положению показывают странные категории вроде YOLO и Absurd. Траты сильно скошены: большинство тратит мало.
[8] · eda['total_spent'] = ... и графики
Гипотеза: клиенты с детьми тратят меньше. Сложил все траты и всех детей с подростками, убрал выброс дохода. Слева ящики с усами: медиана трат без детей 1189, с одним ребёнком 306, с двумя-тремя меньше ста. Справа видно, что траты растут с доходом. Но даже при одинаковом доходе 60-80 тысяч бездетные тратят больше: 1289 против 960. Значит, дети влияют и сами по себе. Гипотеза подтвердилась.
Если спросят: почему медиана?
Траты скошены, несколько очень больших значений тянут среднее вверх. Медиана показывает типичного клиента.
Раздел 3 · Предобработка и kNN
[10] · df = df.drop(columns='id')
Для kNN каждого клиента нужно превратить в вектор чисел. Удаляю id, потому что это просто номер. Образование и семейное положение кодирую через one-hot, чтобы не было ложного порядка между категориями. Дату перевожу в дни и сжимаю min-max в отрезок от нуля до единицы. Потом z-scaling всех признаков кроме дохода, потому что евклидово расстояние складывает квадраты разниц, и признак с большими числами, например траты на вино, задавил бы остальные.
Если спросят: почему income не масштабировал?
Его мы восстанавливаем, по нему соседей не ищут. Его масштабируют позже, отдельной ячейкой [15].
[11] · class CustomKNeighborsRegressor
fit у kNN ничего не обучает, он просто запоминает выборку. В kneighbors считаю через cdist матрицу расстояний: 24 клиента без дохода на 2216 с доходом. Потом argsort по строкам, беру первые три номера и через take_along_axis достаю их расстояния. Всё векторно, без циклов.
[13] · проверка с sklearn и markdown под ней
Расстояния совпали полностью. Номера соседей совпали у 23 клиентов из 24. Разница из-за того, что в данных 201 строка-дубликат: один и тот же клиент записан под разными id. Если две копии стоят на одинаковом расстоянии, моя функция и sklearn берут разные копии. Поэтому я дополнительно сравнил сами векторы соседей: они совпадают, а значит, и заполненный доход тот же.
[14] · neighbor_incomes = train['income'].values[indices]
По номерам соседей достаю их доходы: таблица 24 на 3. Беру среднее по строке и записываю в пропуски. Пропусков осталось ноль.
Раздел 4 · Кластеры
[18] · графики SSE и силуэта
kMeans на шести признаках трат. Чтобы выбрать k, смотрим инерцию и силуэт. Инерция падает всегда, явного локтя нет. Силуэт лучше всего при k=2, но это грубое деление «мало тратит, много тратит». Следующий пик при k=4, 0.47. Его и берём.
[20] · PCA, t-SNE, UMAP
Шесть признаков не нарисовать, поэтому снижаем размерность. PCA линейный, первая ось по сути «сколько всего тратит»: экономные плотной кучей, остальные перекрываются. t-SNE и UMAP нелинейные, они сохраняют соседей. Экономные уходят отдельным облаком, лучше всего группы разделяет UMAP. 3D почти ничего не добавляет.
[21], [22] и markdown «Интерпретация»
Самая большая группа, около 1300 человек, это экономные: тратят мало на всё. Вторая, около 390: вино в 2.4 раза выше среднего и много мяса. Третья, около 345: гурманы, много тратят на фрукты, рыбу и сладости. Самая маленькая, около 200: любители золота, золото втрое выше среднего. На вопрос про кластер любителей вина: да, но честнее назвать его «вино и мясо». Номера кластеров случайные, поэтому описываю группы по размеру.
Раздел 5 · Свой kMeans
[24] · def kmeans_predict
Функция считает через cdist расстояния от каждой точки до каждого центра, это таблица 150 на 3, и argmin по строке даёт номер ближайшего центра.
[26] и картинка [27] · цикл обновления центров
Семь итераций: назначаю точки ближайшему центру, потом каждый центр переношу в среднее его точек. Если кластер пустой, центр не двигаю. Историю центров сохраняю, на картинке видно, как крестики к четвёртому-пятому шагу встают в середины облаков.
[28], [29], [30] · def kmeans_fit_predict и итог
Полная версия: стартовые центры через np.random.uniform от low до high. На каждой итерации назначаю точки, пересчитываю центры, пустой кластер получает новый случайный центр. loss это сумма евклидовых расстояний, на которые сдвинулись центры. Остановка, когда loss меньше tol или кончился max_iter. В конце loss ноль, центры встали, три облака найдены.
Если спросят: почему loss то растёт, то падает?
loss это сдвиг центров, а не качество разбиения. Центры могут сделать большой шаг после маленького, монотонно убывать он не обязан. Убывает сумма квадратов расстояний до центров.
Если спросят: чем хуже sklearn?
Один запуск со случайного старта, поэтому может застрять в локальном минимуме и склеить облака. В sklearn умный старт k-means++, 10 запусков с выбором лучшего и оптимизированный код.
Конец
Вот, в целом, всё. Могу подробнее рассказать любой пункт.
Если скажут «тут можно было без цикла»
В ЛР1 было замечание про перебор. Здесь циклы есть только в двух местах, и на оба есть ответ.
| Где | Что ответить |
|---|---|
for j in range(k) в своём kMeans | Цикл по кластерам, их всего 3. Всё внутри векторное: маска и mean по точкам. Можно убрать и его, например через one-hot матрицу меток, но при k=3 выигрыша нет, а читается хуже. |
for i in range(max_iter) | Это сами итерации алгоритма: каждая зависит от результата предыдущей, их нельзя посчитать разом. |
| kNN | Циклов нет: все расстояния считаются одной матрицей через cdist. |
| Задания pandas и SQL | Без циклов: маски, isin, crosstab, подзапросы. |