Shanraq.org Shanraq.org
Выборка и фильтры: оставить только нужное
IT

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.

Карта урока

Карта урока: маска, строки и столбцы

Скажите своими словами

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

  1. Что такое маска и почему df["amount"] > 10000 — не «да» и не «нет»?
  2. Почему два условия соединяют знаком &, а не словом and?
  3. Почему после фильтра 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))

Задание

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

  1. Чеки дороже 5000 в Костанае — только день и сумма.
  2. Отметьте чеки: крупный, если сумма от 20000, иначе обычный. Столбец добавьте присваиванием, отметку проставьте через loc. Напечатайте, сколько каких.
  3. Сумма крупных чеков по городам.

Ожидаемый вывод:

дороже 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), которые читаются лучше, чем три скобки подряд.

Ответы

Показать ответы

На вопросы

  1. Маска — это столбец из «да» и «нет», по одному ответу на строку, с теми же подписями. Сравнение столбца с числом сравнивает каждое значение, а не столбец целиком, поэтому и ответ получается столбцом.
  2. Потому что and требует одного ответа, а у столбца их столько же, сколько строк. & — знак, который работает поэлементно и возвращает такой же столбец. По той же причине нужны скобки: & выполняется раньше сравнения.
  3. Потому что фильтр не пересчитывает подписи: у оставшихся строк они те же, что были. loc[2] спрашивает про строку с подписью 2, iloc[2] — про третью по порядку, и после фильтра это разные строки.

К разминке

  1. Сравнение даёт столбец из «да» и «нет», sum() считает подошедшие, а amounts[mask] оставляет только их — вместе с подписями.
[False, True, False, True] | подошло: 2
{'топливо': 18500, 'аренда': 95000}
  1. (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
  1. 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 перебивает его там, где условие верно; так не остаётся строк без отметки. Третий — фильтр и группировка: отобрать крупные, а потом сложить по городам.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.