У темі 21 ми зʼясували, що
розбирати CSV рядковими методами не можна, і навчилися робити це чесно — через
csv.DictReader. На виході отримали список словників: кожен рядок файлу став
одним словником, ключі — назви стовпців. Поки питання просте («скільки всього?»), цього
цілком досить. А тепер спробуй сформулювати вголос: середній чек по кожній філії,
без порожніх клітинок, найбільший зверху. Одне речення. Скільки в ньому рядків
коду?
У попередній темі ми отримали масив: багато
однакових чисел в одному обʼєкті й операції відразу над усіма. Це половина потрібного.
Друга половина — імена. У таблиці стовпці називаються словами, типи в них різні («Вокзал»
поруч зі 125), а рядки можуть мати власні позначки, які не збігаються з
номерами по порядку. Саме це й додає pandas: масив, у якого є імена.
Наскрізний приклад — журнал продажів тієї самої кавʼярні з теми 21, тільки тепер йдеться не про меню, а про чеки. Вісім записів, три філії, три напої. Він маленький навмисне: усі числа тут можна перевірити в голові, і саме тому видно, що бібліотека не робить нічого чарівного.
01 / МотивНавіщо, коли є csv
Ось те саме завдання, розвʼязане знайомими засобами. Код чесний, робочий і саме такий, який написав би уважний новачок після теми 21:
Претензій до нього немає. Претензія в іншому: у ньому три структури даних,
яких немає в задачі. Замовник просив середній чек — а в коді зʼявились
суми, скільки і середні, і кожну з них треба
тримати в голові. Тепер уяви, що завтра попросять «тільки капучино» — доведеться додати
if усередині циклу. А післязавтра «і ще по днях тижня» — і словники стануть
словниками словників.
Код росте не лінійно від складності запитання, а швидше. Причина в тому, що ми описуємо шлях — як саме пройти рядки й куди що додати, — замість того щоб описувати результат. Pandas дає мову, в якій пишеш друге:
Рядок читається майже як речення: згрупуй за філією, візьми стовпець суми, усередни,
округли, відсортуй за спаданням. «Тільки капучино» тут — не новий if, а ще
одна ланка в ланцюжку. Ціна цієї стислості — нові поняття, і зараз ми розберемо їх усі
по черзі.
pd — звичайне скорочення при
імпорті, як np для NumPy: import pandas as pd пишуть усі, і
відступати від цієї домовленості немає сенсу — чужий код читатимеш саме в такому вигляді.
Ставиться бібліотека через pip install pandas
(тема 17) і в стандартну поставку не входить.02 / БудоваDataFrame, Series та індекс
У бібліотеці всього два головні типи, і другий є частиною першого.
DataFrame — таблиця. Рядки, іменовані стовпці, у різних
стовпцях різні типи. Це те, що ти бачиш в Excel.
Series — один стовпець. Послідовність значень одного
типу плюс позначки для кожного значення.
Створити таблицю можна просто зі словника, де ключ — назва стовпця, а значення — список:
Друк дає рівно те, чого чекаєш — з одним доважком ліворуч:
Стовпчик 0…7 зліва — не стовпець таблиці. Це індекс
(index): окремий обʼєкт з позначкою для кожного рядка. Саме його немає у звичайній
таблиці, і саме він далі буде джерелом і зручності, і плутанини.
Поки що індекс виглядає нудно, бо він створився сам:
Але індекс — не обовʼязково номери. Це можуть бути номери чеків, дати
(тема 22), артикули, імена. Головне —
індекс не змінюється разом із позицією рядка. Відфільтруєш таблицю,
відсортуєш її, викинеш половину рядків — позначки поїдуть із рядками, і третій зверху
рядок цілком може мати індекс 7. З цієї простої обставини виростає весь
розділ 04.
Схема 1 · Анатомія DataFrame
Три складові таблиці й те, що з неї віддає звертання до одного стовпця.
Series, у якої
залишились позначки рядків. Через це продажі["сума"] > 100 згодом
можна буде підставити назад у таблицю: pandas знатиме, який відповідь стосується
якого рядка.Звертання до стовпця виглядає як звертання до словника — і це не випадковість:
Ліворуч — індекс, праворуч — значення, унизу — імʼя стовпця й тип. Тип
int64 знайомий із теми 33: усередині
кожного стовпця живе звичайний масив NumPy. DataFrame — це набір таких масивів,
поставлених поруч і підписаних.
object.
Масив NumPy зберігає значення однакового розміру підряд, а українські назви напоїв мають
різну довжину. Тому в стовпці філія лежать не самі рядки, а вказівники на
звичайні пітонівські обʼєкти — звідси й назва типу. Наслідок практичний: операції над
таким стовпцем ідуть зі швидкістю Python, а не C. У сучасному pandas є альтернатива —
dtype="string[pyarrow]", яка зберігає текст компактно й працює помітно
швидше.03 / ЧитанняПрочитати файл і роздивитись
Руками таблиці набирають рідко. Звичайний початок роботи — один рядок:
Це той самий файл, який у темі 21 ми відкривали з newline="", віддавали
в csv.DictReader і перетворювали числа руками. Тут усе це вже всередині:
шапка стала назвами стовпців, лапки й екранування розібрані за правилами формату, а
стовпці, де всі значення схожі на числа, отримали числовий тип.
Усі три налаштування з теми 21 нікуди не поділись — вони просто стали іменованими аргументами:
| аргумент | навіщо |
|---|---|
| sep=";" | роздільник не кома — типовий файл з українського Excel |
| encoding="cp1251" | файл не в UTF-8; неправильне значення дає кракозябри або падіння |
| decimal="," | дробові числа записані з комою: 45,50 |
| parse_dates=["день"] | стовпець із датами одразу стає датами, а не текстом |
| na_values=["—", "н/д"] | які саме позначки в цьому файлі означають «немає значення» |
Далі — чотири виклики, з яких починається знайомство з будь-якою незнайомою таблицею. Роби їх завжди, навіть коли впевнений, що знаєш, що всередині.
shape каже, з чим маєш справу: скільки рядків і скільки стовпців. Це не
метод, а властивість — дужок немає:
head(n) показує перші n рядків (за замовчуванням пʼять), щоб
не друкувати мільйон. Є й tail(n) — останні:
info() — найкорисніший з чотирьох. Він відповідає одразу на два питання,
які завжди виникають: де порожні клітинки і які типи прочиталися:
Стовпчик Non-Null Count порівнюй із 8 entries вгорі: де
число менше, там дірки. А Dtype ловить класичну халепу — стовпець, який мав
бути числовим, прочитався як object. Причина майже завжди одна: у файлі
серед чисел трапився нечисловий символ — пробіл між тисячами, знак валюти або та сама
кома замість крапки.
describe() рахує зведення по числових стовпцях. Текстові він за
замовчуванням пропускає:
Читати це варто не зверху вниз, а порівнюючи два рядки: 50% (медіану) з
mean. У стовпці сума середнє 111.9, а медіана 105 — середнє
помітно вище, бо його тягне вгору один чек на 270. Коли ці два числа розходяться,
у даних майже завжди є або кілька дуже великих значень, або помилка введення.
04 / ВибірВибір: loc проти iloc
Із стовпцями все просто. Один стовпець у квадратних дужках дає Series,
список стовпців — нову таблицю:
Подвійні дужки — не описка й не «спеціальний синтаксис». Усередині тих самих
квадратних дужок лежить звичайний список ["філія", "сума"]. Одна назва —
один стовпець, список назв — кілька стовпців.
А от із рядками починається головна плутанина теми. Для них є два способи звертання, і вони не взаємозамінні.
locбере рядок за міткою — за тим значенням, що стоїть в індексі;ilocбере рядок за позицією — за номером зверху, з нуля, як у списку (i— від integer).
Поки індекс — це 0, 1, 2, …, вони дають те саме, і різниці не видно
взагалі. Різниця зʼявляється рівно тоді, коли індекс перестає збігатися з позицією.
А стається це не в екзотичних випадках, а після найзвичайнішої дії — фільтрації:
Чотири рядки, позиції 0, 1, 2, 3 — а індекс 2, 3, 5, 7. Позначки поїхали разом із рядками з початкової таблиці, і тепер у тій самій таблиці існують дві різні системи координат. Пограйся з ними:
Інтерактив 1 · loc і iloc на одному ключі
Таблиця дорогі з попереднього прикладу: індекс 2, 3, 5, 7, позиції 0, 1, 2, 3. Обери ключ і подивись на обидві відповіді.
iloc працює, а loc падає з
KeyError, бо мітки 0 в цій таблиці більше немає.Обидва вміють брати не лише рядок, а й клітинку — через кому: спершу рядки, потім стовпці. І тут ховається ще одна відмінність, про яку легко забути:
Зріз за мітками включає праву межу, на відміну від усіх зрізів, які
ти бачив у Python досі. Це не примха: мітки не обовʼязково числа, і для дат
loc["2024-01-01":"2024-01-31"] природно очікувати, що січень увійде цілком.
Але переплутати легко, тож правило варто запамʼятати окремо.
loc. Знаю номер —
iloc. Якщо ж стало байдуже, бо «вони й так однакові», — це і є той момент,
коли варто перевірити df.index: індекс збігається з позиціями лише в щойно
прочитаній таблиці, до першого фільтра чи сортування.05 / ФільтрФільтр умовою
Фільтрація в pandas виглядає дивно рівно доти, доки не розкласти її на два кроки.
Крок перший: порівняння застосовується до всього стовпця одразу й дає стовпець
відповідей «так/ні» — таку саму Series тієї ж довжини:
Крок другий: цю маску підставляють у квадратні дужки таблиці, і залишаються тільки
рядки, навпроти яких стоїть True:
Тобто продажі[…] означає то «дай стовпець» (якщо всередині рядок-назва),
то «дай рядки» (якщо всередині маска). Це справді два різні значення однакового запису;
pandas розрізняє їх за типом того, що в дужках.
Умов буває кілька, і тут — друга класична пастка теми. Природно написати так:
Причина вже знайома з теми 11. Оператор
and вимагає від кожного боку одну істину чи хибу: він обчислює
лівий вираз, перевіряє його на правдивість і вирішує, чи дивитись на правий. А зліва в
нас вісім значень одразу. На питання «ця Series правдива?» чесної відповіді немає, і
pandas відмовляється вгадувати — тому й пропонує в тексті помилки уточнити:
.any() чи .all().
Потрібні поелементні оператори — ті самі, що в темі 33:
| треба | пиши | не пиши |
|---|---|---|
| і те, і те | & | and |
| або те, або те | | | or |
| навпаки | ~ | not |
І тут одразу третя пастка — дужки. У Python & звʼязує сильніше за
> і ==, тому вираз без дужок читається зовсім не так, як
виглядає:
Тому кожну умову беруть у власні дужки — завжди, без винятків:
Інтерактив 2 · Конструктор умови
Збери фільтр із двох частин і подивись, які рядки лишились. Оператор and тут теж доступний — саме щоб побачити, що з ним стається.
& лишає рядки, де справджуються обидві умови, | — де
хоч одна. Два останні варіанти нічого не повертають: and падає одразу,
а зникнення дужок міняє порядок дій так, що вираз перестає мати сенс. Зверни увагу
й на комбінації, де під & не лишається жодного рядка — порожня
таблиця це не помилка, і pandas про неї не попередить.Коли умова — це перелік значень, замість довгого ланцюжка | беруть
isin:
06 / СтовпціНові стовпці
Новий стовпець створюється присвоєнням — так само, як новий ключ у словнику (тема 08). Праворуч від знака рівності пишуть вираз над цілими стовпцями, без жодного циклу:
Ділення відбулося порядково: перший елемент на перший, другий на другий. Це та сама
векторна арифметика з теми 33, тільки тепер
операнди знаходяться за іменами, а не за номерами. І вирівнюються вони теж
за індексом, а не за позицією: якщо скласти два стовпці з різних
таблиць, pandas зіставить рядки з однаковими мітками, а для міток, що є лише в одному
боці, поставить NaN.
Дві дрібниці, які варто знати одразу. Перша: продажі.сума теж працює й
дає той самий стовпець, але лише поки назва — коректне імʼя Python і не збігається з
методом. Стовпець ціна за шт із пробілом так не візьмеш, а стовпець
count віддасть метод замість даних. Квадратні дужки працюють завжди.
Друга: перейменувати стовпці можна словником, і це перше, що роблять із чужим файлом, де шапка написана як заманеться:
07 / groupbyРозділити, застосувати, зібрати
Ось операція, заради якої pandas і вчать. Питання «скільки виторгу дала кожна філія» розкладається на три кроки, і всі три робить один вираз.
Розділити (split): рядки роз'їжджаються по купках за значенням у
стовпці філія — три рядки в «Центр», три у «Вокзал», два в «Парк».
Застосувати (apply): у кожній купці окремо береться стовпець
сума і згортається в одне число. Зібрати (combine): числа
складаються в нову маленьку таблицю, де індексом стають самі назви філій.
Схема 2 · Розділити → застосувати → зібрати
Той самий вираз продажі.groupby("філія")["сума"].sum(), розкладений на три кроки.
.loc["Вокзал"], а не через номер. І порядок груп у відповіді
алфавітний, а не такий, як у файлі: groupby сортує ключі, поки йому
явно не сказати sort=False.Замість sum може стояти будь-яка згортка: mean,
count, min, max, median,
std, nunique. Якщо не вибирати стовпець, згортка застосується
до всіх числових одразу. А коли потрібні різні згортки різних стовпців — є
agg, де кожен рядок задає одну колонку майбутнього звіту:
Групувати можна й за кількома стовпцями — тоді ключем стає пара значень:
продажі.groupby(["філія", "напій"])["сума"].sum(). Результат матиме
дворівневий індекс; повернути його назад у звичайні стовпці — це
.reset_index().
itertools.groupby з
теми 30. Та функція групувала
лише сусідні однакові ключі, тож дані спершу треба було сортувати вручну —
інакше «Центр» на початку й «Центр» у кінці давали дві окремі групи. Метод
groupby у pandas такої вимоги не має: він сам будує відповідність
«ключ → номери рядків», тому порядок у файлі не має значення. Однакова назва, різна
поведінка — приклад того, чому назву методу завжди варто перевіряти за документацією
саме тієї бібліотеки.08 / ПропускиПропуски й NaN
У справжніх файлах клітинки бувають порожні: касир не пробив суму, прилад не зняв
показник, поле не заповнили. Порожнє місце в CSV pandas читає як
NaN — not a number, спеціальне значення з
стандарту чисел із рухомою крапкою.
Перший наслідок помітний одразу: стовпець із пропуском не може лишитись
int64, бо NaN — це число з рухомою крапкою. Тому цілі
перетворюються на дробові, і замість 3 у друку зʼявляється
3.0.
Другий наслідок дивніший. NaN не дорівнює нічому — навіть самому собі:
Це не помилка реалізації, а свідоме рішення стандарту: NaN означає
«результат невизначений», а два невідомі значення не можна оголосити рівними. Практичний
висновок жорсткий: шукати пропуски порівнянням не можна. Запис
таблиця[таблиця["сума"] == np.nan] завжди дасть порожню таблицю — і не
поскаржиться. Для цього є окремий метод:
Читається так: isna() робить таблицю з True там, де пропуск,
а sum() рахує True як одиниці — виходить кількість дірок у
кожному стовпці. Цей рядок варто писати одразу після read_csv, разом із
info().
Далі — рішення, і воно завжди змістовне, а не технічне. Варіантів два.
Схема 3 · Що роблять dropna і fillna
Одна таблиця з дірками, три погляди на неї — і середнє, яке в кожному випадку різне.
dropna() викинув
ще й рядок 1, у якого сума була відома, а не було лише кількості;
fillna(0) відверто бреше, бо нуль означає «продали на нуль гривень», а
ми просто не знаємо суму. Заповнювати нулями можна лише там, де порожньо справді
означає нуль — наприклад, у стовпці «повернень за день».Викинути рядки. dropna() без аргументів прибирає рядок,
де порожня хоч одна клітинка, — на нашій таблиці з пʼяти рядків лишається два.
Зазвичай це занадто, тому вказують, які саме стовпці критичні:
Заповнити. fillna(значення) ставить на місце пропусків
те, що скажеш. Нуль — найгірший вибір за замовчуванням, бо він тихо тягне середнє вниз.
Чесніші варіанти: медіана стовпця (стійка до викидів), останнє відоме значення
(ffill — доречно для показників у часі) або окрема категорія
"невідомо" для текстових стовпців.
І окремо про третій варіант, який часто найправильніший: не робити
нічого. Згортки в pandas пропуски ігнорують самі. mean() ділить
суму наявних значень на їхню кількість, а не на довжину стовпця, — тому
71.67 виходить і без жодного прибирання, і після
dropna(subset=["сума"]). А от dropna() без аргументів дає вже
62.50: разом із зіпсованими рядками він викинув і той, де бракувало лише кількості.
Витирати дірки треба тоді, коли далі йде операція, яка з ними не працює, — а не «про
всяк випадок».
09 / ПорядокСортування й підрахунок
Сортують не за індексом, а за значенням стовпця:
Індекс поїхав разом із рядками — 5, 7, 2 — і це ще одна нагода згадати різницю
loc й iloc. Сортувати можна за кількома стовпцями одразу,
задавши для кожного свій напрямок:
Якщо потрібні тільки перші кілька, є коротший і швидший спосіб —
продажі.nlargest(3, "сума"): він не сортує всю таблицю, а шукає три
найбільші.
value_counts() відповідає на питання «яких значень скільки» — це
groupby плюс count, згорнуті в один виклик:
Результат уже відсортований за спаданням. Це перший інструмент, яким варто дивитись на будь-який текстовий стовпець: він одразу показує і склад даних, і брудні дані — «Вокзал», «вокзал» і «Вокзал » з пробілом у кінці стануть трьома різними рядками.
10 / mergeДві таблиці разом
Дані рідко лежать в одному файлі. Поруч із журналом продажів є прайс:
merge зшиває дві таблиці за спільним стовпцем: до кожного рядка продажів
підставляється ціна відповідного напою.
Головне питання при зшиванні — що робити з тим, чому не знайшлося пари. Тут «Какао»
є в прайсі, але його ніхто не купив. За замовчуванням how="inner": лишаються
тільки рядки, ключ яких є в обох таблицях, — і «Какао» просто зникає, мовчки. Саме так і
губляться дані у звітах.
| how | що лишиться | рядків у нас |
|---|---|---|
| "inner" | тільки спільні ключі | 8 — «Какао» зникло |
| "left" | усі рядки лівої таблиці | 8 — «Какао» зникло |
| "outer" | усі ключі з обох таблиць | 9 — «Какао» з NaN |
Практичне правило: після кожного merge дивись на shape.
Рядків стало менше, ніж було, — щось випало через inner. Рядків стало
більше — ключ виявився неунікальним, і один рядок зліва зустрів кілька справа.
Обидві несподіванки трапляються постійно й не викликають жодної помилки.
11 / СтильЛанцюжок замість змінних
Кожна операція в pandas повертає нову таблицю, а не змінює наявну. Тому природний стиль тут — не послідовність присвоєнь, а ланцюжок:
Справа не в красі. Проміжні змінні крок1, крок2 залишаються
жити після обчислення, і за півсотні рядків уже незрозуміло, котра з них актуальна.
Ланцюжок не лишає нічого, крім результату, а зовнішні дужки дозволяють розбити його на
рядки й читати згори вниз як список дій. Вставити новий крок посередині — один рядок;
викинути — теж один.
Схема 4 · Одне завдання, дві мови
Середня сума чека по філіях, найбільша зверху. Ліворуч — csv і словники з теми 21, праворуч — pandas.
if усередину циклу й не забути, що він міняє й лічильник; у правому це
один рядок .dropna(subset=["сума"]) у ланцюжку. Ліва версія при цьому
не «погана» — вона просто описує шлях, а не результат, і тому росте разом із
питанням.12 / ГлибшеДля тих, кому мало
Звідки береться SettingWithCopyWarning. Найзагадковіше
попередження pandas виглядає так:
Корінь тут той самий, що у вигляді проти копії в
темі 33. Зріз масиву NumPy — це вікно в чужу памʼять, і запис у нього змінює
оригінал. Але фільтрація за маскою вікном бути не може: відібрані рядки не лежать у
памʼяті підряд. Тому pandas іноді повертає вигляд, а іноді копію — залежно від того,
який саме зріз вийшов, — і сам не знає напевно, чим виявився дорогі. Отже,
не знає й того, чи потрапить нова колонка в продажі. Попередження означає
буквально: «я не можу пообіцяти, що з цього вийде».
Лікується однозначністю. Хочеш окрему таблицю — скажи це прямо:
У сучасних версіях цю двозначність прибирають на рівні бібліотеки: режим
Copy-on-Write, який у pandas 3.0 став єдиним, робить так, що результат
фільтрації завжди поводиться як копія. Попередження зникає разом із
непевністю — але звичка писати .copy() явно лишається корисною, бо робить
намір видимим для читача коду.
Чому apply рядок за рядком повільний. Спокуса написати
таблиця.apply(функція, axis=1) велика: виглядає як звичайний цикл, тільки
коротше. Ним він і є. На кожен рядок pandas створює окремий обʼєкт
Series, викликає твою пітонівську функцію, забирає результат — і так
мільйон разів. Замір на 200 000 рядках:
Різниця майже в три тисячі разів, і береться вона не з оптимізації, а з рівня, на
якому виконується робота: множення двох стовпців іде одним циклом усередині C над
суцільною памʼяттю, без створення пітонівських обʼєктів. Правило: якщо дію можна
виразити арифметикою над стовпцями, np.where, map по словнику
чи методами .str, — apply(axis=1) не потрібен. Він лишається
для випадків, коли інакше ніяк, і тоді його вартість треба просто визнати.
query: умова як текст. Для довгих фільтрів є запис без
дужок і без повторення імені таблиці:
Зверни увагу на and — тут він раптом дозволений. Суперечності немає:
всередині лапок лежить не вираз Python, а рядок, який pandas розбирає власним
аналізатором і перетворює на ті самі поелементні операції. Python цей текст не бачить
взагалі. Зворотний бік — помилку в такому рядку редактор не підсвітить, а звертання до
змінної ззовні вимагає позначки: query("сума > @поріг").
13 / ПідсумокЩо забрати з теми
Головна ідея одна: таблиця — це один обʼєкт, а не список рядків. Як тільки перестаєш писати цикл по рядках, зникає і більшість помилок, які в цьому циклі робились.
DataFrame— таблиця,Series— один стовпець. Усередині кожного стовпця живе масив NumPy, тому арифметика над стовпцями векторна й швидка.- Індекс — це мітки рядків, а не їхні номери. Після фільтра чи сортування він перестає збігатися з позиціями, і саме звідти беруться всі проблеми з вибором.
loc— за міткою,iloc— за позицією. На таблиці з індексом 2, 3, 5, 7 запит[2]дає різні рядки й жодної помилки. Зрізlocвключає праву межу, зрізiloc— ні.- Фільтр — це маска з
TrueіFalse, підставлена в квадратні дужки. Кілька умов зʼєднують&,|,~, і кожну беруть у власні дужки:andпадає зValueError, а без дужок міняється порядок дій. groupby— це розділити, застосувати, зібрати. Ключі групування стають індексом результату, а порядок стає алфавітним.- Пропуски — це
NaN, і він не дорівнює сам собі. Шукати їх треба черезisna();dropnaвикидає рядки,fillnaзаповнює, а згортки на кшталтmeanігнорують дірки самі. - Кожна операція повертає нову таблицю. Звідси ланцюжки замість
проміжних змінних — і звідси ж
SettingWithCopyWarning, коли незрозуміло, вигляд це чи копія. - Після
mergeзавжди дивись наshape. Рядків поменшало — щось випало черезinner; побільшало — ключ неунікальний.
Тепер у нас є таблиця, з якої можна швидко дістати будь-яке число. Але звіт із
трьох чисел ніхто не читає очима, а звіт із трьохсот — тим паче. Дані треба
побачити: чи росте виторг, чи є викиди, як розподілені суми чеків.
У темі 35 ми навчимося малювати — і почнемо саме з того місця, де
зупинились: groupby віддасть числа, а matplotlib перетворить
їх на картинку.
practice.ipynb ти збереш
DataFrame руками, запишеш його у справжній CSV у тимчасовій теці й прочитаєш назад через
read_csv; переконаєшся assert-ом, що loc і
iloc на відфільтрованій таблиці дають різні рядки; спіймаєш
ValueError від and і KeyError від
loc; порахуєш groupby двома способами — бібліотечним і руками
через словник — і доведеш, що числа збігаються; подивишся, як dropna і
fillna міняють середнє; і прибереш за собою тимчасову теку.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.