ЕГЭ · Информатика · Базы данных и таблицы
Многотабличные базы и связи
Связи между таблицами и внешний ключ — суть задания 3.
🎯 ЕГЭ информатика: этот урок закрывает задание(я) 3.
- ⚠Считают по одной таблице, забывая связь по ключу со второй
- ⚠Путают ID и IDродителя при поиске предков/потомков в родословных
Зачем базу разрезают на несколько таблиц
С чего начинается тема
Задание 3 — единственное в первой части ЕГЭ, где вам выдают файл с настоящей базой данных и просят достать из неё число. Не пересказать определение «реляционная модель», не нарисовать схему — посчитать сумму, количество или найти человека. Всё решается на компьютере, и решается за три-четыре минуты, если вы понимаете, что именно с чем связано.
Почему вообще данные лежат в нескольких таблицах, а не в одной? Ответ на этот вопрос и есть тема урока — и он же подсказывает алгоритм решения. В базе «Молочные продукты», которую ФИПИ гоняет из варианта в вариант, три таблицы: «Движение товаров», «Товар», «Магазин». Цена лежит в одной таблице, район — в другой, количество упаковок — в третьей. Ни одного вопроса нельзя решить по одной таблице: любой ответ требует склеить записи по ключу.
Именно на этом теряют балл. Ученик открывает файл, видит столбец «Количество упаковок», фильтрует по датам, суммирует — и получает количество штук вместо суммы в рублях. Или суммирует всё подряд, не заметив, что половина строк — это поступления, а не продажи.
Поэтому урок устроен так: сначала разбираемся, почему таблиц несколько и что такое связь, потом решаем задание 3 двумя способами — формулами в электронной таблице и программой на Python, — и в обоих случаях идём по строкам с трассировкой, чтобы было видно, какая строка попала в ответ, а какая отсеялась и почему.
Одна большая таблица и три её болезни
Представьте, что мы хотим обойтись одной таблицей. В каждой строке — всё сразу: дата операции, адрес магазина, район, наименование товара, цена за упаковку, количество, тип операции. Выглядит удобно: ничего никуда не надо склеивать.
Беда начинается на второй тысяче строк.
Избыточность. Ряженка продавалась триста раз — значит, строка «Ряженка 4%, 0,5 л, 78 руб.» лежит в файле триста раз. Данные, которые по смыслу существуют в одном экземпляре, хранятся сотнями копий. Место — полбеды; хуже другое.
Аномалия обновления. Цена ряженки выросла с 78 до 82 рублей. Теперь её надо поправить в трёхстах строках. Поправили в двухстах девяноста восьми — и база стала противоречивой: в ней одновременно написано, что ряженка стоит 78 и что она стоит 82. Никакой запрос больше не даёт правильного ответа, потому что правильного ответа в базе нет.
Аномалии вставки и удаления. В сеть привезли новый товар, но он ещё ни разу не продавался. Куда его записать? В единственной таблице строка появляется только вместе с операцией — значит, товар нельзя завести, пока его не продали. Это аномалия вставки. Обратная беда: если удалить все операции по магазину, из базы исчезнет и сам магазин с его адресом. Это аномалия удаления.
Лекарство от всех трёх — одно и то же: каждый факт хранить ровно в одном месте, а остальные строки на него ссылаться. Именно это и называется многотабличной базой.
Как большую таблицу разрезают на три
Разрезание идёт по простому правилу: всё, что относится к одной сущности, уезжает в свою таблицу.
Что в нашей базе является сущностью? Товар — да: у него есть собственные свойства (наименование, единица измерения, фасовка, цена), которые не зависят ни от какой операции. Магазин — да: район и адрес принадлежат магазину, а не продаже. Операция — тоже да: у неё своя дата, свой объём, свой тип.
Получаются три таблицы.
«Товар»: артикул, наименование, единица измерения, количество в упаковке, цена за упаковку. Каждый товар — ровно одна строка.
«Магазин»: ID магазина, район, адрес. Каждый магазин — ровно одна строка.
«Движение товаров»: ID операции, дата, ID магазина, артикул, количество упаковок, тип операции. Здесь строк много, но в каждой нет ни цены, ни района — только ссылки на них.
Обратите внимание, что произошло с ценой. Раньше она стояла в каждой строке о продаже; теперь — в одной строке таблицы «Товар». Изменение цены — правка одной ячейки, и противоречию взяться неоткуда. Ровно так же с районом: он записан один раз рядом с магазином.
Плата за это — необходимость соединять таблицы при любом вопросе, где нужны данные из разных мест. Задание 3 на экзамене — это ровно такое соединение, и ничего больше.
Определение
Внешний ключ — поле одной таблицы, ссылающееся на ключ другой. Через связи «склеиваем» данные: по строке о продаже находим и название товара, и район магазина.
Первичный ключ и внешний ключ: чем они отличаются
Два термина, которые путают чаще всего, хотя различие между ними простое.
Первичный ключ — поле (или набор полей), которое однозначно определяет строку своей таблицы. В таблице «Товар» это артикул: артикул 103 — это ровно одна строка, и никакая другая. В таблице «Магазин» — ID магазина. Требования к первичному ключу два: значения не повторяются и не бывают пустыми.
Внешний ключ — поле чужой таблицы, значения которого берутся из первичного ключа своей. В «Движении товаров» внешних ключей два: «Артикул» смотрит в «Товар», «ID магазина» смотрит в «Магазин». Значения внешнего ключа, наоборот, повторяются сколько угодно: артикул 103 встретится в сотне операций, и это нормально — именно так выражается связь «один товар — много операций».
Отсюда практическое правило чтения схемы: сторона «один» — там, где ключ первичный; сторона «многие» — там, где он внешний. На схеме базы данных, которую ФИПИ показывает картинкой рядом с условием, ключевое поле подчёркнуто, а связи нарисованы линиями. Линия всегда идёт от подчёркнутого поля к неподчёркнутому — от «одного» к «многим».
Ещё одно следствие, полезное на экзамене: по внешнему ключу вы всегда идёте в сторону справочника и находите ровно одну строку. По артикулу 103 находится один товар и одна цена. Обратный путь неоднозначен: у товара 103 операций много, и «найти операцию по артикулу» — это уже не одна строка, а список.
Типы связей
- • один товар — много поставок
- • одной записи — ровно одна
- • через промежуточную таблицу
Почему «многие-ко-многим» требует третьей таблицы
Связь «один-ко-многим» умещается в два поля: в таблице на стороне «многие» заводят внешний ключ. А вот «многие-ко-многим» так не записывается, и понять почему стоит один раз.
Пример: ученики и кружки. Один ученик ходит на несколько кружков, на один кружок ходит несколько учеников. Попробуем обойтись двумя таблицами. Добавим в «Ученика» поле «Кружок» — но кружков у него три, а поле одно. Заводить «Кружок 1», «Кружок 2», «Кружок 3»? Тогда четвёртый кружок сломает структуру базы, а у большинства учеников два поля из трёх будут пустыми.
Выход — третья таблица, в которой каждая строка означает один факт участия: «Записи», два поля — ID ученика и ID кружка. Ученик с тремя кружками даёт три строки. Новый кружок — новая строка, структура базы не меняется.
Эта промежуточная таблица распадается на две обычные связи «один-ко-многим»: один ученик — много записей, один кружок — много записей. Поэтому в реляционной базе связи «многие-ко-многим» физически не существует: она всегда представлена парой связей через промежуточную таблицу.
В базе «Движение товаров» промежуточная таблица уже есть, только её не называют промежуточной: сама таблица операций связывает товары с магазинами многие-ко-многим и попутно хранит собственные данные операции — дату, количество, тип.
Осторожно с направлением связи: количество упаковок и цена лежат в РАЗНЫХ таблицах. Чтобы посчитать выручку, соедини запись о продаже (сколько упаковок) с записью о товаре (цена). Логика решения: фильтр → соединение → агрегат
Как задание 3 выглядит на экзамене
Что именно даёт вам компьютер
Условие линии 3 начинается словами «Задание выполняется с использованием прилагаемого файла». Файл — электронная таблица с тремя листами: «Движение товаров», «Товар», «Магазин». В листе операций обычно несколько сотен строк, и просмотреть их глазами нельзя — в этом весь смысл задания.
Дальше идёт описание таблиц, схема связей картинкой и единственный вопрос. Формулировка из реальных вариантов звучит так: «определите, на какую сумму (в руб.) был продан товар „Кефир 1%“ в магазинах Центрального района за период с 13 по 21 октября включительно. Стоимость продажи одной операции равна произведению количества проданных упаковок на цену за упаковку».
Разберём эту фразу по кусочкам — каждый кусочек превращается в условие отбора.
— «был продан» → тип операции «Продажа», поступления не считаем; — «товар „Кефир 1%“» → наименование из таблицы «Товар», а в операциях лежит только артикул, значит нужна связь по артикулу; — «в магазинах Центрального района» → район из таблицы «Магазин», в операциях лежит ID магазина, значит нужна вторая связь; и учтите, что районом «Центральный» могут быть отмечены несколько магазинов; — «с 13 по 21 октября включительно» → отбор по дате, оба конца диапазона входят; — «произведению количества на цену» → сумма произведений, а не сумма количеств.
Пять условий, два из которых требуют выхода в соседние таблицы. Схема решения всегда одна: подтянуть недостающие поля по ключу → отфильтровать → просуммировать произведение.
Решение формулами в электронной таблице
Разбор примера
Задание 3 в электронной таблице: ВПР и СУММПРОИЗВ
База «Молочные продукты». Лист «Движение товаров» (12 строк данных, строки 2–13, столбцы A–F: ID операции, Дата, ID магазина, Артикул, Количество упаковок, Тип операции), лист «Товар» (строки 2–6, столбцы A–E: Артикул, Наименование, Едизм, Количество в упаковке, Цена за упаковку) и лист «Магазин» (строки 2–6, столбцы A–C: ID магазина, Район, Адрес).
Определите, на какую сумму (в руб.) был продан товар «Кефир 1%» в магазинах Центрального района за период с 13 по 21 октября включительно.
Показать решение по шагам
- 1. Шаг 1. Подтягиваем район. В листе «Движение товаров» встаём в свободный столбец G, в ячейку G2, и пишем:
=ВПР(C2; Магазин!$A$2:$B$6; 2; ЛОЖЬ). ЗдесьC2— ID магазина из строки операции,Магазин!$A$2:$B$6— справочник (обязательно с долларами, иначе при протягивании диапазон уедет вниз),2— номер столбца справочника, из которого берём значение (район),ЛОЖЬ— требование точного совпадения. Протягиваем G2 до G13. - 2. Шаг 2. Подтягиваем наименование и цену. В H2:
=ВПР(D2; Товар!$A$2:$E$6; 2; ЛОЖЬ)— второй столбец таблицы «Товар» это наименование. В I2:=ВПР(D2; Товар!$A$2:$E$6; 5; ЛОЖЬ)— пятый столбец, цена за упаковку. Протягиваем обе формулы до 13-й строки. Теперь в каждой строке операции стоит всё, что нужно для отбора: район (G), наименование (H) и цена (I). - 3. Шаг 3. Считаем одной формулой. В любой свободной ячейке:
=СУММПРОИЗВ((F2:F13="Продажа") * (G2:G13="Центральный") * (H2:H13="Кефир 1%") * (ДЕНЬ(B2:B13)>=13) * (ДЕНЬ(B2:B13)<=21) * E2:E13 * I2:I13). - 4. Шаг 4. Как это читается. Каждая скобка со сравнением даёт массив из ЛОЖЬ и ИСТИНА, то есть из нулей и единиц. Перемножение массивов — это логическое «И» по каждой строке: единица останется только там, где выполнены все пять условий сразу. Дальше произведение умножается на количество упаковок и на цену, и СУММПРОИЗВ складывает результат по всем строкам. Строки, не прошедшие отбор, дают множитель 0 и в сумму не попадают.
- 5. Шаг 5. Что проходит отбор. Из двенадцати строк условиям удовлетворяют пять: 13.10 (8 упаковок), 16.10 (6 упаковок), 18.10 (11 упаковок), 20.10 (3 упаковок) и 21.10 (4 упаковок). Всего 8 + 6 + 11 + 3 + 4 = 32 упаковки.
- 6. Шаг 6. Умножаем на цену кефира: 32 · 62 = 1984.
Ответ: 1984 руб.
Разбор формул: почему именно так, а не иначе
Почему ВПР, а не просто взгляд в соседнюю таблицу. Строк операций на экзамене несколько сотен, и вручную вы не сопоставите ни одну. ВПР делает ровно то, что описано в условии словами «поле „Артикул“ ссылается на „Артикул“ таблицы „Товар“»: берёт значение внешнего ключа и находит по нему строку справочника. Это и есть соединение таблиц, выполненное вручную.
Почему четвёртый аргумент ЛОЖЬ. Без него ВПР ищет приблизительное совпадение и требует, чтобы справочник был отсортирован. На неотсортированном справочнике приблизительный поиск молча возвращает не ту строку — цену чужого товара. Ошибка не видна: формула не ругается, просто ответ неверный. Пишите ЛОЖЬ (или 0) всегда.
Почему доллары. $A$2:$B$6 — абсолютная ссылка, она не смещается при протягивании. Напишете A2:B6 — в двенадцатой строке формула будет искать в диапазоне A13:B17, где справочника уже нет, и вернёт ошибку или пустоту.
Почему СУММПРОИЗВ, а не СУММЕСЛИМН. СУММЕСЛИМН умеет складывать один столбец по нескольким условиям, но не умеет складывать произведение двух столбцов. А в задании 3 нужна именно сумма произведений: количество на цену. СУММПРОИЗВ для этого и сделан.
Про даты. ДЕНЬ(B2:B13) работает, только если в столбце B лежат настоящие даты, а не текст. Если ФИПИ отдал даты текстом (так бывает), формула вернёт ошибку; тогда проще отсортировать лист по дате и взять диапазон строк, либо перевести столбец в даты через «Текст по столбцам». И помните: при работе с одним месяцем сравнивать достаточно день, но если диапазон пересекает границу месяца, сравнивать нужно дату целиком.
Что унести из урока
Многотабличная база — это способ хранить каждый факт ровно один раз. Цена товара живёт в таблице «Товар», район — в таблице «Магазин», а операции только ссылаются на них внешними ключами. Плата за такое хранение — необходимость соединять таблицы при каждом вопросе, и задание 3 ЕГЭ проверяет ровно это умение.
Первичный ключ однозначно определяет строку своей таблицы и не повторяется. Внешний ключ — поле, значения которого берутся из первичного ключа другой таблицы, и он повторяется сколько угодно; сторона «многие» в связи — всегда та, где ключ внешний. Связь «многие-ко-многим» в реляционной базе не существует напрямую: она всегда разворачивается в третью таблицу, каждая строка которой — один факт.
Решение любой задачи линии 3 укладывается в три действия: подтянуть недостающие поля по ключу, отфильтровать строки по всем условиям вопроса, посчитать итог. В электронной таблице это ВПР плюс СУММПРОИЗВ, на Python — словари-справочники плюс один цикл с continue. Второй способ надёжнее, когда условий много или когда база родословная.
И последнее, что стоит проверять перед записью ответа: не перепутан ли тип операции, взят ли весь район, а не один магазин, входят ли обе границы диапазона дат и умножено ли количество на цену. Четыре вопроса, десять секунд — и балл остаётся при вас.
Вопрос на проверку
Почему в ВПР четвёртым аргументом почти всегда пишут ЛОЖЬ?
Ответить и проверить себя — после бесплатной регистрации.
То же задание на Python
Зачем решать задание 3 программой
Формулы быстрее, когда вопрос простой. Но линия 3 умеет усложняться: «сумма по району, где продано больше всего упаковок», «средняя цена по отделу», родословные с поиском внуков. Там, где условие перестаёт укладываться в одну строку формулы, программа надёжнее, потому что её можно писать по шагам и печатать промежуточные результаты.
Читать файл базы данных на Python проще всего библиотекой openpyxl — она входит в состав любого школьного дистрибутива с Python и умеет открывать .xlsx. Главный приём один: превратить каждый справочник в словарь, где ключ — первичный ключ таблицы, а значение — то, что нам из этой таблицы нужно. После этого соединение таблиц превращается в обычное обращение по ключу, а вся задача — в один проход по таблице операций.
Три строчки на словарь, один цикл на подсчёт — и никаких протягиваний формул.
Разбор примера
Решение на openpyxl: код и полная трассировка
Тот же вопрос на тех же данных: сумма продаж товара «Кефир 1%» в магазинах Центрального района с 13 по 21 октября включительно.
Показать решение по шагам
- 1.
import openpyxl
book = openpyxl.open('baza.xlsx', readonly=True)
# Справочник товаров: артикул -> (наименование, цена за упаковку) tovar = {} for art, name, unit, pack, rub in book['Товар'].iter_rows(min_row=2, values_only=True): tovar[art] = (name, rub)# Справочник магазинов: ID магазина -> район rayon = {} for mid, r, adres in book['Магазин'].iter_rows(min_row=2, values_only=True): rayon[mid] = rsumma = 0 for op, data, mid, art, kol, tip in book['Движение товаров'].iter_rows(min_row=2, values_only=True): if tip != 'Продажа': continue if rayon[mid] != 'Центральный': continuename, rub = tovar[art]
if name != 'Кефир 1%': continue if not (13 <= data.day <= 21): continue summa += kol * rubprint(summa)
- 2. Шаг 1.
iter_rows(min_row=2, values_only=True)идёт по строкам листа начиная со второй (первая — заголовки) и отдаёт каждую строку кортежем значений. Кортеж сразу распаковывается в переменные по числу столбцов — порядок переменных обязан совпадать с порядком столбцов на листе. - 3. Шаг 2. Первые два цикла строят словари-справочники. После них
tovar— это{101: ('Кефир 1%', 62), 102: ('Сливки 10%', 95), 103: ('Ряженка 4%', 78), 104: ('Молоко 3,2%', 71), 105: ('Творог 5%', 88)}, аrayon—{1: 'Центральный', 2: 'Ленинский', 3: 'Кировский', 4: 'Центральный', 5: 'Ленинский'}. Это и есть соединение таблиц: теперь по любому артикулу цена достаётся за одно обращение. - 4. Шаг 3. Главный цикл идёт по операциям и четырьмя
continueотбрасывает всё лишнее.continueудобнее вложенныхif: каждое условие проверяется отдельно, и при отладке легко поставитьprintперед любым из них, чтобы увидеть, что именно отсеялось. - 5. Шаг 4.
data.dayберёт день месяца. openpyxl отдаёт даты объектамиdatetime, если в файле они записаны как даты. Если же дата пришла строкой'13.10.2024', тоdata.dayне сработает, и день добывают так:int(data.split('.')[0]). Проверять тип стоит печатью:print(type(data))в первой итерации. - 6. Шаг 5.
summa += kol * rubнакапливает произведение количества на цену — ровно то, что требует условие. - 7. Шаг 6. Итог печатается один раз после цикла: 1984.
Ответ: 1984
| 12.10, магазин 1, арт. 101, 8 упаковок, Продажа | тип ✔; район «Центральный» ✔; «Кефир 1%» ✔; день 12 < 13 ✘ → отброшена. summa = 0 |
|---|---|
| 13.10, магазин 1, арт. 101, 8 упаковок, Продажа | все четыре условия ✔ → summa = 0 + 8 · 62 = 496 |
| 14.10, магазин 1, арт. 101, 15 упаковок, Поступление | тип не «Продажа» ✘ → отброшена на первом же continue. summa = 496 |
| 15.10, магазин 2, арт. 101, 9 упаковок, Продажа | район магазина 2 — «Ленинский» ✘ → отброшена. summa = 496 |
| 16.10, магазин 4, арт. 101, 6 упаковок, Продажа | магазин 4 — тоже «Центральный» ✔, всё совпало → summa = 496 + 6 · 62 = 868 |
| 17.10, магазин 1, арт. 102, 7 упаковок, Продажа | артикул 102 — «Сливки 10%» ✘ → отброшена. summa = 868 |
| 18.10, магазин 4, арт. 101, 11 упаковок, Продажа | всё ✔ → summa = 868 + 11 · 62 = 1550 |
| 19.10, магазин 3, арт. 101, 5 упаковок, Продажа | магазин 3 — «Кировский» ✘ → отброшена. summa = 1550 |
| 20.10, магазин 1, арт. 101, 3 упаковок, Продажа | всё ✔ → summa = 1550 + 3 · 62 = 1736 |
| 21.10, магазин 1, арт. 101, 4 упаковок, Продажа | 21 — верхняя граница, «включительно» ✔ → summa = 1736 + 4 · 62 = 1984 |
| 20.10, магазин 4, арт. 103, 9 упаковок, Продажа | артикул 103 — «Ряженка 4%» ✘ → отброшена. summa = 1984 |
| 22.10, магазин 4, арт. 101, 12 упаковок, Продажа | день 22 > 21 ✘ → отброшена. Итог: 1984 |
Три ошибки, которые стоят балла
Трассировка выше подобрана так, что каждая отброшенная строка — это ловушка из реальных вариантов. Пройдём по ним ещё раз.
Забыли тип операции. Строка от 14.10 — поступление на 15 упаковок. Если не отфильтровать тип, к ответу добавится 15 · 62 = 930 рублей, и вместо 1984 выйдет 2914. Это самая частая потеря балла в линии 3: слово «продан» в вопросе легко проскочить глазами.
Взяли один магазин вместо района. «Центральный район» — это магазины 1 и 4. Ученик, который нашёл в справочнике первую строку с нужным районом и дальше фильтровал по ID 1, потеряет операции 16.10 и 18.10, то есть 6 · 62 + 11 · 62 = 1054 рубля. Правило: фильтруем по району, а не по ID магазина, и список подходящих ID сначала выписываем целиком.
Промахнулись по границе диапазона. «С 13 по 21 включительно» означает, что 13-е и 21-е входят. Операции 12.10 и 22.10 — соседи границ с обеих сторон, и обе должны отсеяться. Если написать строгое неравенство 13 < data.day < 21, потеряются и 13-е, и 21-е число: 8 · 62 + 4 · 62 = 744 рубля мимо ответа.
Четвёртая, менее заметная ошибка — сложить количества вместо денег. 32 упаковки тоже выглядят как правдоподобный ответ, и проверить себя просто: если в вопросе есть слово «руб.», ответ обязан получаться умножением на цену.
Вопрос на проверку
В условии сказано «за период с 13 по 21 октября включительно». Какая проверка дня в коде верна?
Ответить и проверить себя — после бесплатной регистрации.
Вторая разновидность: родословные
Таблица, которая ссылается сама на себя
Вторая база, которую ФИПИ использует в линии 3, — родословная. В ней тоже две таблицы, но связь устроена хитрее.
Таблица «Люди»: ID, ФамилияИ.О., Пол, Год рождения. Первичный ключ — ID.
Таблица «Родственные связи»: IDРодителя, IDРебёнка. Оба поля — внешние ключи, и оба смотрят в одну и ту же таблицу «Люди». Такая связь называется рекурсивной: сущность связана сама с собой.
Вот почему её нельзя записать иначе. У человека двое родителей, а детей может быть сколько угодно — значит, связь «родитель — ребёнок» это многие-ко-многим, а такие связи, как мы разобрали, всегда живут в отдельной таблице. Строка «(1, 3)» читается как один факт: «человек 1 — родитель человека 3».
Дальше вся родня выражается через эту одну связь.
— Дети человека X: все IDРебёнка в строках, где IDРодителя равен X. — Родители X: все IDРодителя в строках, где IDРебёнка равен X. — Внуки X: дети его детей, то есть шаг вниз повторён дважды. — Правнуки X: три шага вниз. — Братья и сёстры X: дети его родителей, кроме самого X. — Двоюродные братья и сёстры X: дети братьев и сестёр его родителей.
Заметьте общий приём: любое родство — это путь по связям заданной длины и в заданном направлении. Умея делать один шаг вниз и один шаг вверх, вы соберёте любое родство из условия.
Разбор примера
Сколько внуков: код и трассировка по шагам
Таблица «Люди»: 1 — Иванов А.А. (М, 1946), 2 — Иванова Б.Б. (Ж, 1948), 3 — Иванов В.А. (М, 1970), 4 — Иванова Г.А. (Ж, 1972), 5 — Петров Д.С. (М, 1971), 6 — Иванов Е.В. (М, 1995), 7 — Иванова Ж.В. (Ж, 1998), 8 — Петрова З.Д. (Ж, 1996), 9 — Иванов И.Е. (М, 2020).
Таблица «Родственные связи» (IDРодителя, IDРебёнка): (1, 3), (2, 3), (1, 4), (2, 4), (3, 6), (3, 7), (4, 8), (5, 8), (6, 9).
Сколько внуков у Иванова А.А.?
Показать решение по шагам
- 1.
import openpyxl
book = openpyxl.open('rodoslovnaya.xlsx', readonly=True)
# Люди: ID -> (фамилия, пол, год рождения) lyudi = {} for pid, fio, pol, god in book['Люди'].iter_rows(min_row=2, values_only=True): lyudi[pid] = (fio, pol, god)# Дети: ID родителя -> список ID детей deti = {} for rod, reb in book['Родственные связи'].iter_rows(min_row=2, values_only=True): deti.setdefault(rod, []).append(reb)# Ищем ID нужного человека по фамилии start = [pid for pid, (fio, pol, god) in lyudi.items() if fio == 'Иванов А.А.'][0]vnuki = set() for syn in deti.get(start, []): for vnuk in deti.get(syn, []): vnuki.add(vnuk)print(len(vnuki), sorted(vnuki))
- 2. Шаг 1. Строим словарь
deti. Идём по девяти строкам связей. Пара (1, 3): ключа 1 в словаре ещё нет,setdefaultкладёт пустой список и добавляет 3 — получается{1: [3]}. Пара (2, 3):{1: [3], 2: [3]}. Пара (1, 4): ключ 1 уже есть, 4 дописывается —{1: [3, 4], 2: [3]}. Пара (2, 4):{1: [3, 4], 2: [3, 4]}. - 3. Шаг 2. Продолжаем: (3, 6) и (3, 7) дают
3: [6, 7]; (4, 8) даёт4: [8]; (5, 8) даёт5: [8]; (6, 9) даёт6: [9]. Итоговый словарь:{1: [3, 4], 2: [3, 4], 3: [6, 7], 4: [8], 5: [8], 6: [9]}. - 4. Шаг 3.
start— ID Иванова А.А., это 1. Внешний цикл идёт по его детям:deti[1]равно[3, 4]. - 5. Шаг 4. Первая итерация внешнего цикла,
syn = 3. Внутренний цикл берётdeti.get(3)— это[6, 7]. В множество добавляются 6 и 7. Теперьvnuki = {6, 7}. - 6. Шаг 5. Вторая итерация,
syn = 4.deti.get(4)— это[8]. Добавляется 8. Теперьvnuki = {6, 7, 8}. Цикл закончился. - 7. Шаг 6. Ответ —
len(vnuki), то есть 3: Иванов Е.В., Иванова Ж.В. и Петрова З.Д. - 8. Шаг 7. Проверка, где мог быть промах. Человек 9 (Иванов И.Е., 2020) в ответ не входит: он ребёнок человека 6, то есть правнук, а не внук — три шага вниз вместо двух. И обратите внимание на
deti.get(syn, [])вместоdeti[syn]: у бездетного человека ключа в словаре нет, и обычное обращение упало бы с ошибкойKeyError.
Ответ: 3 внука (ID 6, 7, 8)
Как из этого кода получить любое родство
Код из разбора меняется под любой вопрос почти без переписывания — меняется только направление и длина пути.
Шаг вверх. Нужен второй словарь: roditeli.setdefault(reb, []).append(rod). Тот же цикл по связям, только ключ и значение поменялись местами. Через него находятся родители, бабушки и дедушки (два шага вверх), братья и сёстры.
Братья и сёстры. Это дети родителей, кроме самого человека: brat = set(), дальше по каждому родителю из roditeli[X] добавляем всех его детей, а в конце brat.discard(X). Забыть discard — типичная ошибка: человек попадает в список собственных братьев, и ответ больше правильного на единицу.
Все потомки, а не только внуки. Когда в условии написано «прямые потомки» без указания поколения, фиксированного числа шагов не хватает: дерево уходит вниз на неизвестную глубину. Обходят его очередью:
potomki = set()
ochered = list(deti.get(start, []))
while ochered:
x = ochered.pop()
if x not in potomki:
potomki.add(x)
ochered += deti.get(x, [])
Цикл забирает человека из очереди, отмечает его как потомка и дописывает в очередь его детей. Когда очередь опустела, обойдено всё поддерево. Проверка if x not in potomki обязательна: без неё человек, до которого ведут два разных пути, обработается дважды.
Фильтр по году или полу. Добавляется в самом конце, когда множество уже собрано: len([p for p in potomki if lyudi[p][2] > 1980]). Считать сразу в цикле опаснее — легко отсечь ветку, через которую шёл путь к нужным людям.
Вопрос на проверку
В словаре deti = {1: [3, 4], 3: [6, 7], 4: [8], 6: [9]} сколько у человека 1 внуков?
Ответить и проверить себя — после бесплатной регистрации.
Универсальная схема решения линии 3 в одну строку: справочники в словари → один проход по операциям → четыре continue → накопитель. Меняется только содержимое условий.
Вопрос с развёрнутым ответом
Объясни, зачем данные разносят по нескольким связанным таблицам, а не хранят всё в одной большой таблице.
Ответить и проверить себя — после бесплатной регистрации.
Разбор примера
Задание 3 с группировкой: в каком районе продали больше всего
По той же базе «Молочные продукты» определить, в каком районе было продано наибольшее количество упаковок товара «Ряженка 4%» (артикул 103), и сколько именно упаковок.
Показать решение по шагам
- 1. Шаг 1. Отличие от разобранной задачи в том, что ответ надо получить не одним числом, а по группам. Накопитель поэтому не один, а по одному на каждый район — то есть словарь.
- 2.
from collections import defaultdict
itogi = defaultdict(int) for op, data_op, mid, art, kol, tip in book['Движение товаров'].iter_rows(min_row=2, values_only=True): if tip != 'Продажа': continue if tovar[art][0] != 'Ряженка 4%': continueitogi[rayon[mid]] += kol
print(dict(itogi)) print(max(itogi, key=itogi.get), max(itogi.values())) - 3. Шаг 2.
defaultdict(int)заводит новый район со значением 0 автоматически, при первом обращении. Без него пришлось бы писать проверку «есть ли уже такой ключ», и именно там обычно и теряется первая продажа каждого района. - 4. Шаг 3. Трассировка по строкам таблицы операций. Строка 1 (17.10, магазин 3, артикул 103, 25 упаковок) — тип «Поступление», отброшена первым же
continue. - 5. Шаг 4. Строка 2 (18.10, магазин 3, 103, 7) — продажа ряженки, магазин 3 в Кировском районе: itogi['Кировский'] = 7.
- 6. Шаг 5. Строка 3 (19.10, магазин 3, артикул 105) — это творог, а не ряженка: отброшена вторым
continue. - 7. Шаг 6. Строки 4, 6, 7 и 8 (магазин 3, артикул 103, по 12, 6, 10 и 8 упаковок) добавляются в Кировский: 7 + 12 + 6 + 10 + 8 = 43.
- 8. Шаг 7. Строка 5 (22.10, магазин 1, 103, 9) — магазин 1 в Центральном районе: itogi['Центральный'] = 9. Строка 9 (21.10, магазин 2, 103, 15) — Ленинский: 15. Строка 10 — артикул 104, молоко, мимо.
- 9. Шаг 8. Итог словаря: Кировский 43, Ленинский 15, Центральный 9. Наибольшее значение у Кировского района.
- 10. Шаг 9. Ответ: Кировский район, 43 упаковки.
- 11. Шаг 10. Про
max(itogi, key=itogi.get). Обычныйmax(itogi)вернул бы наибольший ключ по алфавиту, а не район с наибольшей суммой — это классическая ошибка. Аргументkeyговорит, по какому признаку сравнивать: здесь по значению словаря. - 12. Шаг 11. Тот же приём работает для любого вопроса «по группам»: район, отдел, месяц, магазин. Меняется только то, что стоит ключом словаря, — всё остальное в программе остаётся прежним.
Ответ: Кировский район, 43 упаковки
Задание №3 в формате экзамена
Ответить и проверить себя — после бесплатной регистрации.
Задание №3 в формате экзамена
Ответить и проверить себя — после бесплатной регистрации.