Python: от данных до своей сводки Урок 28 из 56
Выборка и фильтры: оставить только нужное
Двадцать седьмой урок курса по Python. Маска — это столбец из «да» и «нет», и ею таблица фильтруется целиком. Разбираем `df[маска]`, `.loc[маска, столбцы]`, `&`, `|`, `~` и обязательные скобки, `isin`, `between`, `str`, а также подписи, которые после фильтра остаются прежними.
Зачем это нужно
Таблица редко нужна целиком. Нужны чеки одного города, расходы за вторую половину месяца, строки, где сумма подозрительно велика. В списках это цикл с if и новый список; в таблице — одно выражение, и оно читается как условие, а не как перебор.
Здесь же закрывается обещание двадцать пятого урока: loc и iloc расходятся ровно там, где строки отфильтровали.
Сразу целиком
Файл vyborka.py. Десять чеков за январь и восемь способов взять из них нужное.
"""Урок 27: взять нужные строки и столбцы.
Десять чеков за январь. Из них выбираем — сначала столбцы, потом строки, потом
и то и другое сразу.
"""
import pandas as pd
rows = [
("2026-01-03", "Костанай", "еда", 4200),
("2026-01-05", "Костанай", "топливо", 18500),
("2026-01-07", "Рудный", "еда", 2600),
("2026-01-09", "Костанай", "связь", 4990),
("2026-01-12", "Астана", "еда", 7300),
("2026-01-15", "Костанай", "аренда", 95000),
("2026-01-18", "Рудный", "топливо", 16200),
("2026-01-21", "Астана", "связь", 4990),
("2026-01-24", "Костанай", "еда", 3100),
("2026-01-27", "Рудный", "аренда", 62000),
]
df = pd.DataFrame(rows, columns=["day", "city", "kind", "amount"])
df["day"] = pd.to_datetime(df["day"])
print(df)
print()
print("== столбцы: один и несколько")
print("df['amount'] →", type(df["amount"]).__name__)
print("df[['city', 'amount']] →", type(df[["city", "amount"]]).__name__)
print()
print("== маска — это столбец из «да» и «нет»")
big = df["amount"] > 10000
print(big.tolist())
print("подошло строк:", int(big.sum()), "из", len(df))
print()
print("== фильтр строк")
print(df[big])
print()
print("== два условия: & | ~ и обязательные скобки")
kostanay_food = (df["city"] == "Костанай") & (df["kind"] == "еда")
print("Костанай и еда:", int(kostanay_food.sum()))
print("не еда:", int((~(df["kind"] == "еда")).sum()))
print("еда или связь:", int(df["kind"].isin(["еда", "связь"]).sum()))
print()
print("== строки и столбцы одним обращением")
print(df.loc[kostanay_food, ["day", "amount"]])
print()
print("== ещё три готовых условия")
print("от 4000 до 20000:", int(df["amount"].between(4000, 20000).sum()))
print("город на «К»:", int(df["city"].str.startswith("К").sum()))
print("вторая половина месяца:", int((df["day"] >= "2026-01-15").sum()))
print()
print("== подписи после фильтра не пересчитываются")
rudny = df[df["city"] == "Рудный"]
print("подписи:", rudny.index.tolist())
print("loc[2]:", rudny.loc[2, "kind"], "| iloc[2]:", rudny.iloc[2]["kind"])
print()
print("== запись через loc")
df.loc[df["kind"] == "связь", "kind"] = "мобильная связь"
print(df["kind"].value_counts().to_dict())
Выводит:
day city kind amount
0 2026-01-03 Костанай еда 4200
1 2026-01-05 Костанай топливо 18500
2 2026-01-07 Рудный еда 2600
3 2026-01-09 Костанай связь 4990
4 2026-01-12 Астана еда 7300
5 2026-01-15 Костанай аренда 95000
6 2026-01-18 Рудный топливо 16200
7 2026-01-21 Астана связь 4990
8 2026-01-24 Костанай еда 3100
9 2026-01-27 Рудный аренда 62000
== столбцы: один и несколько
df['amount'] → Series
df[['city', 'amount']] → DataFrame
== маска — это столбец из «да» и «нет»
[False, True, False, False, False, True, True, False, False, True]
подошло строк: 4 из 10
== фильтр строк
day city kind amount
1 2026-01-05 Костанай топливо 18500
5 2026-01-15 Костанай аренда 95000
6 2026-01-18 Рудный топливо 16200
9 2026-01-27 Рудный аренда 62000
== два условия: & | ~ и обязательные скобки
Костанай и еда: 2
не еда: 6
еда или связь: 6
== строки и столбцы одним обращением
day amount
0 2026-01-03 4200
8 2026-01-24 3100
== ещё три готовых условия
от 4000 до 20000: 6
город на «К»: 5
вторая половина месяца: 5
== подписи после фильтра не пересчитываются
подписи: [2, 6, 9]
loc[2]: еда | iloc[2]: аренда
== запись через loc
{'еда': 4, 'топливо': 2, 'мобильная связь': 2, 'аренда': 2}
Разбор
Столбец и список столбцов
df["amount"] — это Series, один столбец. df[["city", "amount"]] — это DataFrame из двух: внутри квадратных скобок список, и ответ поэтому таблица, даже если имя в списке одно. Разница важна: у Series и DataFrame разные методы, и половина вопросов «почему не работает» — это Series там, где ждали таблицу.
Маска — это столбец из «да» и «нет»
df["amount"] > 10000 не отвечает «да» или «нет». Оно отвечает столбцом: по одному ответу на строку, с теми же подписями. Отсюда всё остальное:
big.sum()считает подошедшие строки, потому чтоTrue— это единица (четвёртый урок);df[big]оставляет строки, где стоитTrue;- маска сопоставляется с таблицей по подписям, а не по порядку, — как и всё в pandas.
&, |, ~ и скобки, которые не украшение
Два условия соединяются знаками, а не словами: & вместо and, | вместо or, ~ вместо not. Слова здесь не работают, и это не каприз: and требует одного ответа «да» или «нет», а у столбца их десять. Python честно говорит об этом:
ValueError: The truth value of a Series is ambiguous.
Скобки вокруг каждого условия обязательны, потому что & выполняется раньше сравнения: без них df["city"] == "Костанай" & df["kind"] == "еда" посчитается как df["city"] == ("Костанай" & df["kind"]) == "еда" — и упадёт, не объяснив, что виноваты скобки.
Готовые условия вместо длинных цепочек
Три вещи, которые пишут каждый день:
df["kind"].isin(["еда", "связь"])— вместо(... == "еда") | (... == "связь"), и список может быть любой длины;df["amount"].between(4000, 20000)— вместо двух сравнений, границы включены;df["city"].str.startswith("К")— и рядомstr.contains,str.lower,str.len: всё, что умеют строки, у столбца есть через.str.
Пропуски проверяют отдельно: df["amount"].isna() и .notna(). Сравнение с NaN всегда даёт «нет», даже NaN == NaN, поэтому искать пропуски сравнением бесполезно.
Строки и столбцы одним обращением
df.loc[маска, ["day", "amount"]] — левая часть выбирает строки, правая столбцы. Это и короче, и дешевле, чем df[маска][["day", "amount"]]: второй вариант строит промежуточную таблицу из всех столбцов только затем, чтобы выбросить лишние.
Есть и словесная форма: df.query("amount > 5000 and city == 'Костанай'") — внутри строки как раз работают and и or. Она удобна для длинных условий, которые вы написали сами.
Чего с ней делать нельзя: собирать строку запроса из того, что ввёл пользователь. query разбирает выражение как код, и подставленный текст выполнится — это та же дыра, что и SQL-инъекция из двадцать первого урока. Значения передают через @переменная (df.query("amount > @limit")), а условие, пришедшее извне, безопаснее собирать масками — они ничего не выполняют.
Подписи после фильтра не пересчитываются
Отфильтровали три строки — подписи остались [2, 6, 9], потому что это те же самые строки. Поэтому loc[2] и iloc[2] в примере дают разное: одно спрашивает про строку с подписью 2, другое — про третью по счёту.
Это то самое место, про которое двадцать пятый урок говорил «разница появится, как только строки отфильтруют». Если подписи мешают — reset_index(drop=True) пронумерует строки заново; drop=True здесь потому, что иначе старые подписи станут новым столбцом.
Запись через loc, и почему не через фильтр
Отметить строки, подошедшие под условие, нужно через loc:
df.loc[df["kind"] == "связь", "kind"] = "мобильная связь"
Вариант df[df["kind"] == "связь"]["kind"] = "мобильная связь" выглядит логично и ничего не делает. df[маска] возвращает новую таблицу, и присваивание меняет её, а не исходную. pandas это замечает и печатает ChainedAssignmentError — предупреждение, которое легко пропустить в длинном выводе, потому что уходит оно не туда, где вы ждёте вывод программы.
Правило простое: если слева от = больше одних квадратных скобок — это loc.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Что такое маска и почему
df["amount"] > 10000— не «да» и не «нет»? - Почему два условия соединяют знаком
&, а не словомand? - Почему после фильтра
loc[2]иiloc[2]могут дать разные строки?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает эта программа?
import pandas as pd
amounts = pd.Series([4200, 18500, 2600, 95000], index=["еда", "топливо", "еда", "аренда"])
mask = amounts > 5000
print(mask.tolist(), "| подошло:", int(mask.sum()))
print(amounts[mask].to_dict())
2. Заполните пропуск. Вместо ... поставьте условие: город Костанай и сумма больше 5000.
# два условия, каждое в своих скобках
import pandas as pd
df = pd.DataFrame({"city": ["Костанай", "Рудный", "Костанай"], "amount": [4200, 16200, 95000]})
print(df[...].to_string(index=False))
3. Почините. Программа должна переименовать связь в «мобильная связь», но таблица не меняется, а в стороне появляется ChainedAssignmentError.
# присваивание уходит в копию, а не в таблицу
import pandas as pd
df = pd.DataFrame({"kind": ["еда", "связь"], "amount": [4200, 4990]})
df[df["amount"] > 4500]["kind"] = "мобильная связь"
print(df.to_string(index=False))
Задание
Обязательное. Возьмите те же десять чеков из урока и ответьте на три вопроса выборками, а не циклами:
- Чеки дороже 5000 в Костанае — только день и сумма.
- Отметьте чеки:
крупный, если сумма от 20000, иначеобычный. Столбец добавьте присваиванием, отметку проставьте черезloc. Напечатайте, сколько каких. - Сумма крупных чеков по городам.
Ожидаемый вывод:
дороже 5000 в Костанае:
day amount
2026-01-05 18500
2026-01-15 95000
размер чека:
размер
обычный 8
крупный 2
крупные по городам:
city
Костанай 95000
Рудный 62000
Готово, когда: вывод совпадает построчно; отметка проставлена через df.loc[маска, "размер"], а не через фильтр слева от =; в первом ответе выбраны два столбца одним обращением; суммы получены группировкой отфильтрованной таблицы, а не циклом.
На своих данных. Возьмите любую свою таблицу и составьте к ней три условия: одно простое, одно из двух частей и одно с isin или between. Напечатайте, сколько строк подошло под каждое. Если под какое-то подошло ноль — посмотрите dtypes. Когда в столбце лежат строки вместо чисел, == молча даёт «нет» для всех строк, а > и < падают с TypeError. Лечится это не подбором условия, а pd.to_numeric до фильтра.
По желанию.
- Напишите то же условие через
queryи сравните, что читается легче. - Сделайте
df[маска].reset_index(drop=True)и посмотрите, что стало с подписями. - Проверьте, сколько строк даёт
~маска, и убедитесь, что вместе с маской выходит длина таблицы.
Куда это встанет в проекте
Шага сейчас нет: сводке хватает пяти строк, и фильтровать в ней пока нечего. Но выборка — это то, чем будет сделан следующий шаг: когда рядов станет несколько, отчёт начнётся с «взять только нужные строки», а потом уже группировка из следующего урока.
Долги. Условия у нас пишутся прямо в коде. Когда их станет больше трёх, они захотят имени — и превратятся в функции вида krupnye(df), которые читаются лучше, чем три скобки подряд.
Ответы
Показать ответы
На вопросы
- Маска — это столбец из «да» и «нет», по одному ответу на строку, с теми же подписями. Сравнение столбца с числом сравнивает каждое значение, а не столбец целиком, поэтому и ответ получается столбцом.
- Потому что
andтребует одного ответа, а у столбца их столько же, сколько строк.&— знак, который работает поэлементно и возвращает такой же столбец. По той же причине нужны скобки:&выполняется раньше сравнения. - Потому что фильтр не пересчитывает подписи: у оставшихся строк они те же, что были.
loc[2]спрашивает про строку с подписью 2,iloc[2]— про третью по порядку, и после фильтра это разные строки.
К разминке
- Сравнение даёт столбец из «да» и «нет»,
sum()считает подошедшие, аamounts[mask]оставляет только их — вместе с подписями.
[False, True, False, True] | подошло: 2
{'топливо': 18500, 'аренда': 95000}
(df["city"] == "Костанай") & (df["amount"] > 5000). Скобки обязательны, знак —&, а не слово.
import pandas as pd
df = pd.DataFrame({"city": ["Костанай", "Рудный", "Костанай"], "amount": [4200, 16200, 95000]})
print(df[(df["city"] == "Костанай") & (df["amount"] > 5000)].to_string(index=False))
city amount
Костанай 95000
df.loc[df["amount"] > 4500, "kind"] = "мобильная связь". Присваивание должно попасть в саму таблицу, аdf[маска]— это уже другая таблица.
import pandas as pd
df = pd.DataFrame({"kind": ["еда", "связь"], "amount": [4200, 4990]})
df.loc[df["amount"] > 4500, "kind"] = "мобильная связь"
print(df.to_string(index=False))
kind amount
еда 4200
мобильная связь 4990
К заданию
Первый ответ — df.loc[маска, ["day", "amount"]]: строки и столбцы за одно обращение. Второй — сначала столбец целиком заполняется значением по умолчанию, потом loc перебивает его там, где условие верно; так не остаётся строк без отметки. Третий — фильтр и группировка: отобрать крупные, а потом сложить по городам.
Источники
- Индексирование и выбор данных — маски,
loc,ilocи правила, по которым они работают. - Строковые методы столбца — всё, что доступно через
.str. - Возвращать копию или вид — почему присваивание через фильтр ничего не меняет.
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.