Робота з pandas: перехід від SQL до більш гнучкого аналізу даних
Хоча SQL та інструменти BI домінують у аналізі даних, pandas часто залишається недооціненим, незважаючи на свою універсальність. Після десятиліття роботи з ним, я побачив, скільки ще більше він пропонує, крім базових операцій. Багато професіоналів з даними обмежуються простими групуваннями та з'єднаннями, не помічаючи потужних можливостей, таких як нечітке з'єднання, фільтрація, подібна до SQL, та гнучкі зведені таблиці.
Перед тим як ми почнемо, я рекомендую вам пройти цей тест, щоб перевірити свої навички і зрозуміти методи, які будуть охоплені в посібнику.
Гнучкість pandas є як силою, так і викликом. Хоча він дозволяє використовувати кілька підходів, ця гнучкість може бути заплутаною для користувачів SQL, які звикли до структурованих методів.
Цей посібник допомагає усунути розрив між жорстким підходом SQL та гнучкістю pandas. Я покажу, як відтворити основні операції SQL в pandas, вивчити можливості, що виходять за межі SQL, і поділюся своїми найкращими практиками для написання чистого та ефективного коду.
Хто отримає користь від цього посібника:
- Користувачі SQL, які знайомі з pandas, але не впевнені, як використати його весь потенціал.
- Користувачі pandas, які прагнуть освоїти передові техніки та розширити свої навички аналізу даних.
Важливо: Pandas та SQL є взаємодоповнюючими інструментами. Найкраща практика — використовувати SQL для попередньої обробки, комбінування та агрегування великих наборів даних, а pandas для аналізу за запитом, створення спеціальних метрик та візуалізацій. Здатність pandas безперешкодно з'єднуватися з базами даних робить цей робочий процес дуже ефективним.
Початок роботи
Цей посібник структурований як блокнот Jupyter, де всі результати повністю відтворювані. Повний блокнот доступний на GitHub.
Ми будемо працювати з реальним футбольним (soccer) набором даних з Kaggle, що містить дані з Transfermarkt — провідної платформи для футбольної статистики. Ми розглянемо результати ігор, оцінки гравців та статистику змагань. Цей багатий набір даних допоможе нам продемонструвати можливості pandas, а також виявити цікаві інсайти про гру.
Набір даних можна завантажити у вигляді ZIP-файлу з Kaggle або отримати через інструменти, такі як kagglehub
або opendatasets
(для цього необхідно мати акаунт Kaggle та API-токен).
import opendatasets as od
od.download('https://www.kaggle.com/datasets/davidcariboo/player-scores')
Отримання даних з Kaggle
Набір даних містить 10 таблиць, що охоплюють різні аспекти футбольної статистики. Для цього посібника ми зосередимося на підмножині: результати матчів, оцінки гравців на ринку, події матчів і деталі про змагання та клуби (показано приклади записів нижче).
Таблиці набору даних Transfermarkt
Ось короткий опис таблиць, які ми використовуватимемо в цьому посібнику:
games
: Результати матчів, формації, відвідуваність (ID домашніх/виїзних клубів, голи, імена, менеджери). Один рядок — один матч.club_games
: подібно доgames
, але кожен рядок — гра з перспективи одного з клубів, тобто містить в два рази більше рядків, ніжgames
.player_valuations
: Оцінки гравців на ринку, оцінки Transfermarkt, кожна оцінка може оновлюватися кілька разів на рік.competitions
: Інформація про лігу та кубок (назва, код, тип).clubs
: Назви футбольних клубів.
Перехід від SQL до pandas
Розпочнемо з основних функцій, які відображають поширені операції SQL у pandas. Хоча pandas має офіційну документацію, що порівнює синтаксис SQL і pandas, вона зосереджена переважно на базових операціях.
У цьому посібнику я хочу продемонструвати більш просунуті функції та практичні застосування, які виходять за межі цих вступних посібників.
Нижче ми розглянемо, як SQL-подібні операції працюють у pandas, починаючи з основних завдань, таких як фільтрація та групування.
Якщо ви новачок у pandas, я рекомендую почати з туторіалу для початківців, наприклад, пандас туторіал від Kaggle.
Поза базовою фільтрацією — ефективна фільтрація за допомогою .query()
Давайте почнемо з однієї з найпоширеніших SQL операцій — конструкції WHERE
— та її найближчого аналога в Pandas. Розглянемо просте завдання: вибір фіналів Ліги чемпіонів з набору даних games
. Хоча офіційний посібник pandas рекомендує використовувати булеву індексацію, цей підхід може стати громіздким, коли потрібно працювати з кількома умовами:
# Традиційне булеве маскування (важче читати та підтримувати)
cl_finals = games[(games['round'] == 'Final') & (games['competition_id'] == 'CL')]
Хоча це працює, є очевидні недоліки: трикратне повторення назви датафрейму, необхідність додаткових дужок і зростаюча складність із додаванням нових умов. Ми розглянемо більш елегантне рішення за допомогою методу .query()
в Pandas.
cl_finals = games.query("round == 'Final' and competition_id == 'CL'")
Метод .query()
пропонує кращу читабельність і покращену продуктивність на великих таблицях завдяки NumExpr — швидкому числовому виразному оцінювачу, який прискорює .query()
, зменшуючи використання пам'яті та ефективно використовуючи кеш CPU. Для детальнішого вивчення того, як працює NumExpr, ознайомтеся з цією статтею про вивчення ролі NumExpr в Pandas або документацією pandas.
Як видно, .query()
працює швидше за стандартну булеву індексацію, заощаджуючи 40% часу в цьому випадку. Однак поліпшення продуктивності залежить від обсягу вхідних даних. Зазвичай різниця стає більш помітною на великих наборах даних і складних фільтрах, тоді як для менших випадків .query()
може не дати жодних переваг або навіть працювати трохи повільніше.
Окрім швидкості та читабельності, .query()
має й додаткові корисні функції:
- Ми можемо використовувати змінні Python у запитах за допомогою префікса @:
.query("goals > @min_goals")
- Строкові операції працюють плавно:
.query("club_name.str.contains('United')")
- Складні умови легко пишуться і читаються:
.query("(goals > 3 or wins > 10) and year == 2023")
Тепер, коли ми розглянули базову фільтрацію, давайте перейдемо до більш складних операцій, таких як групування та агрегація.
Альтернативи GROUP BY для підрахунку: .value_counts() та .groupby()
Підрахунок частоти в межах категорій — це поширене завдання в аналізі даних. Якщо користувачі SQL використовують COUNT() ... GROUP BY
, то Pandas пропонує дві потужні альтернативи: value_counts()
та groupby().size()
. Розглянемо, як ці методи працюють і коли кожен із них використовувати.
# Метод 1: Використання groupby().size() - корисно для групування по кількох стовпцях
games.groupby('competition_type').size()
# Метод 2: Використання value_counts() - переважно для підрахунку одного стовпця
games.competition_type.value_counts()
groupby().size()
повертає кількість рядків для кожного унікального значення у вказаному стовпці(ах). Воно зберігає оригінальний порядок рядків і добре працює, коли потрібно групувати по кількох стовпцях одночасно.
value_counts()
є більш спеціалізованим — він підраховує унікальні значення в одному стовпці і автоматично сортує результати у порядку спадання. Він також пропонує додаткові функції через параметри, такі як normalize=True
, щоб отримати відсотки.
Альтернативи GROUP BY у pandas
За замовчуванням, як .value_counts()
, так і .groupby().size()
виключають пропущені значення.
Наприклад, якщо 9,7% ігор не мають записаних даних про формації, ці значення будуть опущені з результатів, що може призвести до неправильного тлумачення даних, якщо не вказано dropna=False
.
.valuecounts() метод за замовчуванням приховує пропущені значення_
Обидва методи підраховують кількість повторень, але value_counts()
має кілька переваг: автоматичне сортування, вбудовані пропорції з normalize=True
, простіший синтаксис і швидший час виконання, як показано нижче. Це робить його кращим вибором для швидкого аналізу частоти в окремих стовпцях, тоді як groupby()
корисний для підрахунку за кількома вимірами.
.valuecounts() та .groupby() — порівняння часу виконання_
Освоєння складних агрегацій за допомогою .groupby()
та .agg()
Припустимо, нам потрібно підрахувати кількість ігор за кожний сезон. Найбільш очевидний підхід виглядатиме так: games.groupby(‘season’).game_id.count()
Простий приклад групування
Хоча цей підхід працює добре, у нього є два ключові обмеження:
- Імена стовпців агрегацій залишаються такими ж, як і у оригінальних, що вимагає ручного перейменування для зручності.
- Кожен стовпець може мати лише один метод агрегації.
На практиці нам часто потрібні кілька метрик — наприклад, загальна кількість ігор, середня відвідуваність, загальна кількість забитих голів і коефіцієнт перемог — все це в одній операції. Тут на допомогу приходить комбінація .groupby()
і .agg()
в pandas, що пропонує SQL-подібну гнучкість з інтуїтивним синтаксисом та зручним обробленням кількох агрегацій.
(
games.query("season > 2015")
.groupby("season")
.agg(
total_games=("game_id", "count"),
average_attendance=("attendance", "mean"),
median_attendance=("attendance", "median"),
home_club_goals_per_game=("home_club_goals", "mean"),
away_club_goals_per_game=("away_club_goals", "mean"),
)
# створення нового стовпця з загальною кількістю голів
.eval("total_goals = home_club_goals_per_game + away_club_goals_per_game")
.round(2)
)
Метод .agg()
з іменованими обчисленнями має кілька переваг:
- Чітке найменування метрик замість оригінальних імен стовпців (немає потреби в подальшому перейменуванні).
- Можливість виконання кількох агрегацій на одному стовпці без складності з багатокроковими індексами.
- Чудово працює з пайпінгом, дозволяючи нам поетапно виконувати операції для зручності читання коду.
- В поєднанні з
.eval()
або.assign()
, ми можемо легко додавати нові метрики поверх вже створених (це не завжди можливо в SQL базах даних).
Подивимося, як це працює на прикладі порівняння кількох високорівневих метрик за останнє десятиліття:
Високорівневі агреговані статистики для кожного сезону
Огляд агрегованих даних дозволяє помітити деякі чіткі тенденції:
- Загальна кількість голів за гру поступово зростала з 2.85 (2015) до 3.13 у 2023 році (сезон 2024 ще не завершено).
- Середня відвідуваність сильно відновилася після зниження 2020 року через COVID, досягнувши нових максимумів більше 20 000 у сезонах 2022–23.
- Команди, що грають вдома, зберігають стабільну перевагу в забитих голах, хоча різниця дещо зменшується.
Метод .agg()
створив чисту, добре структуровану таблицю, готову до подальшого аналізу — ми можемо легко візуалізувати її за допомогою бібліотек Python, таких як Plotly, скопіювати в таблиці або продовжити трансформувати її за допомогою додаткових операцій Pandas.
.pivot_table — за межами базових можливостей SQL
Хоча SQL чудово справляється з агрегацією даних, створення зведених таблиць часто вимагає перенесення даних до таблиць у електронних таблицях.
Pandas спрощує цей процес завдяки своєму потужному методу .pivot_table
, пропонуючи прямий шлях від сирих даних до висновків у широкому форматі.
Розглянемо, як змінюються шаблони забитих голів у різних типах змагань, спираючись на наше спостереження за зростанням результативності за останнє десятиліття.
# Створення зведеної таблиці, що показує середню кількість голів за сезон і тип змагань
(
games.query("season>2015")
.assign(total_goals=lambda x: x["home_club_goals"] + x["away_club_goals"])
.pivot_table(
index="season",
columns="competition_type",
values="total_goals",
aggfunc="mean",
margins=True,
margins_name="Totals",
)
.round(2)
)
Чудовою особливістю .pivot_table
є параметр margins
. Встановлення margins=True
та вказівка margins_name
(в даному випадку "Totals"
) додає підсумкові рядки та стовпці до зведеної таблиці — подібно до підсумків у електронних таблицях. Це за лаштунками поєднує вихід .pivot_table
з результатом .groupby
, що усуває необхідність виконувати ці операції окремо. Це дозволяє отримати як детальний розподіл, так і загальні тенденції в одному вигляді.
Середня кількість голів за гру в різних типах змагань
Використовуючи .pivot_table
, ми можемо швидко виявити цікаву тенденцію: національні кубки є найрезультативнішими змаганнями з майже 4 голами за гру і зростаючою результативністю, в той час як матчі національних ліг залишаються стабільними на рівні близько 2.8 голів. Завдання, яке вимагало б кількох SQL-запитів або ручної роботи в електронних таблицях, розкривається в одній чистій таблиці.
Розширені операції об'єднання в Pandas: поза базовим злиттям
Хоча користувачі SQL знайомі з операцією JOIN
, Pandas пропонує ще потужніші можливості злиття, що виходять за межі простого співставлення за ключами. Давайте розглянемо ці функції, починаючи з основ і переходячи до більш складних технік.
Базове об'єднання за допомогою .merge
Основний метод для об'єднання даних в Pandas — це .merge()
, який працює подібно до SQL-операції JOIN
. Хоча Pandas також має метод .join()
для об'єднання за індексами, .merge
є більш гнучким, оскільки дозволяє об'єднувати за будь-яким стовпцем:
# Приклад лівого злиття двох таблиць в pandas
games.merge(
competitions,
left_on=["competition_id"], # можна включити більше стовпців
right_on=["competition_id"],
how="left", # може бути 'inner', 'outer', 'right'
)
Розширене об'єднання часових рядів за допомогою .merge_asof
Реальний аналіз даних часто вимагає більш тонких підходів до об'єднання, особливо коли працюємо з часовими рядами. Розглянемо цей сценарій: ми хочемо проаналізувати, як загальна оцінка гравців футбольної команди впливає на ймовірність перемоги в матчах. Тому нам потрібно зіставити оцінки гравців та дати матчів. Однак оцінки гравців оновлюються лише кілька разів на рік, що робить точні збіги дат неможливими. Отже, нам потрібно знайти найближчу оцінку перед кожною грою.
Однією з типових проблем в спортивній аналітиці є збіг оцінок гравців з датами матчів. Оскільки оцінки не оновлюються щодня, потрібно знайти найближчу оцінку перед кожною грою.
Тут на допомогу приходить метод .merge_asof
. Він виконує "плавне" об'єднання на основі наближеності — ідеально підходить для часових рядів, де точні збіги неможливі. На відміну від звичайного .merge
, він вимагає:
- Дані повинні бути відсортовані за колонкою наближеності
2.
Два ключові параметри:
on="date"
- колонка для наближеного співставлення, в нашому випадку це дата останнього оновлення оцінки трансферу гравцяby="player_id"
- колонка(и) для точного співставлення
# зчитування даних
game_lineups = pd.read_csv("player-scores/game_lineups.csv")
player_valuations = pd.read_csv("player-scores/player_valuations.csv")
# конвертуємо дати у datetime і сортуємо
game_lineups["date"] = pd.to_datetime(game_lineups["date"])
player_valuations["date"] = pd.to_datetime(player_valuations["date"])
# сортуємо за датою, щоб забезпечити правильне вирівнювання для об'єднання за часом
game_lineups = game_lineups.sort_values(by=["date"])
player_valuations = player_valuations.sort_values(by=["date"])
# об'єднання за часом для співставлення кожного складу гри з останньою оцінкою гравців
game_lineups_valuation = pd.merge_asof(
game_lineups,
player_valuations,
by="player_id",
on="date",
direction="backward", # Використовуємо найбільш актуальну оцінку перед грою
)
Перегляд злитих даних дає деякі дивовижні висновки про гроші та успіх у футболі: команді потрібно бути на 40% більшою за вартістю, ніж її суперник, щоб мати більше 50% шансів на перемогу, і навіть перевага у 6–7 разів у оцінці призводить до менш ніж 80% ймовірності перемоги.
Це допомагає пояснити, чому футбол залишається непередбачуваним і захоплюючим — гроші допомагають, але це далеко не все.
Співвідношення порівнює оцінки команд, що грають вдома та на виїзді — співвідношення 1 означає рівні значення, а 2 вказує на те, що команда, яка грає вдома, коштує в два рази більше за гостей.
Хоча SQL вимагає складних віконних функцій та крос-злиттів для виконання наближеного співставлення за часом, метод .merge_asof
в Pandas пропонує більш елегантне і ефективне рішення. Завдяки вбудованому "не точному" співставленню, цей метод спрощує завдання аналізу часових рядів, які в іншому випадку вимагали б десятків рядків SQL-коду. Це особливо корисно при роботі з фінансовими даними, показниками з датчиків чи будь-якими сценаріями, де потрібно співставляти записи на основі найближчої попередньої/майбутньої/найближчої часової мітки.
Елегантні альтернативи віконним функціям SQL в pandas
Віконні функції є одними з найпотужніших, але складних функцій SQL. Хоча pandas не пропонує єдину прямолінійну альтернативу віконним функціям, вона надає елегантні альтернативи, які досягають тих самих результатів з чистим, зрозумілим кодом. Давайте розглянемо кілька популярних випадків використання віконних функцій (агрегації, ранжування/номер рядка та lead/lag) та їх аналоги в pandas, зосередивши увагу на практичних застосуваннях у аналізі даних.
Метод Transform: Потужна альтернатива агрегаціям вікон
Один з найпоширеніших способів використання віконних функцій — це обчислення агрегацій (як сума, середнє або кількість) за конкретними групами, зберігаючи при цьому оригінальну гранулярність даних. Метод transform()
в pandas є відмінним рішенням для цього.
Ми продовжуємо роботу з даними Transfermarkt і будуємо на вже отриманих даних про ринкову оцінку гравців на момент кожного матчу їх клубу.
Цього разу ми будемо обчислювати ринкову вартість кожного гравця як відсоток від загальної вартості їх команди на кожному матчі. Ось як це могло б виглядати в SQL:
SELECT
*,
market_value_in_eur /
SUM(market_value_in_eur) OVER (PARTITION BY game_id, club_id)
AS share_of_player_valuation
FROM player_valuations
Хоча цей результат можна було б отримати, використовуючи метод .merge
, .transform
пропонує більш елегантне рішення. Коли .transform
поєднується з .groupby
, він може повернути Series, який відповідає розміру і індексу оригінального датафрейму. Це означає, що ви можете безпосередньо присвоїти результат як новий стовпець без додаткових операцій злиття.
Це не тільки робить код чистішим, але й покращує продуктивність, усуваючи накладні витрати на операції злиття.
game_lineups_valuation['total_team_valuation_for_game'] = (
game_lineups_valuation
.groupby(['game_id', 'club_id']) # визначаємо вікно для групування
.market_value_in_eur # стовпець для агрегації
.transform('sum') # функція агрегації
)
Давайте порівняємо продуктивність цих підходів:
Порівняння часу виконання groupby та merge проти transform
Порівняння показує, що .transform()
працює в 4,6 рази швидше, ніж підхід з .groupby
і .merge
. Ця різниця в продуктивності стає значною при роботі з великими наборами даних.
Але швидкість — це не єдина перевага. .transform()
дійсно виділяється в трьох ключових аспектах:
- Легке оновлення стовпців: на відміну від операцій злиття, які створюють дублікати стовпців з суфіксами, transform оновлює або створює нові стовпці безпосередньо на місці.
- Підрахунок унікальних значень: хоча віконні функції SQL зазвичай не підтримують
COUNT(DISTINCT)
, в Pandas підрахунок унікальних значень в межах груп дуже простий.
# Підрахунок унікальних гравців для кожного клубу
game_lineups_valuation.groupby('club_id')['player_id'].transform('nunique')
3. Користувацькі обчислення: .transform()
працює як зі стандартними агрегаціями (як сума та середнє), так і з користувацькими функціями. Ось як можна стандартизувати значення в межах груп:
# Стандартизація оцінок трансферу гравців в межах кожної команди
game_lineups_valuation['market_value_normalized'] = (
game_lineups_valuation
.groupby('club_id')['market_value']
.transform(lambda x: (x - x.mean()) / x.std() )
)
Примітка про користувацькі функції
Коли ви використовуєте
.transform()
з користувацькими функціями, поведінка результату залежить від значення, яке повертає ваша функція:Якщо функція повертає Series/масив того ж розміру, що і вхід (як у прикладі зі стандартизацією вище),
.transform()
зберігає ту саму рівноважність рядків.Але якщо функція повертає одне значення,
.transform()
буде працювати як.groupby()
.
Ранжування — аналоги RANK та ROW_NUMBER в pandas
SQL-функції ранжування є важливими інструментами в аналізі даних, пропонуючи ефективні рішення для поширених аналітичних завдань, таких як виявлення кращих виконавців або знаходження останніх записів. Хоча користувачі SQL знайомі з функціями ROW_NUMBER()
, RANK()
та DENSE_RANK()
, Pandas також надає потужні і гнучкі реалізації цих функцій.
Розглянемо кілька поширених аналітичних сценаріїв і як функції ранжування в Pandas можуть допомогти вирішити їх.
- Витягнення даних / видалення дублікатів — отримання останніх 5 результатів матчів для кожного футбольного клубу:
-- SQL
SELECT * FROM (
SELECT
*,
ROW_NUMBER() OVER
(PARTITION BY club_id ORDER BY date DESC) as rn
FROM club_games
)
WHERE rn <= 5
# pandas
(
club_games
.sort_values('date', ascending = True)
.groupby('club_id')
.tail(5)
)
Хоча це не є прямим аналогом віконних функцій, цей код досягає того ж результату, що і SQL-умова WHERE для функції ROW_NUMBER()
. Ключовим моментом є порядок сортування — .sort_values('date', ascending=True)
забезпечує хронологічне упорядкування записів, після чого .tail(5)
вибирає 5 останніх матчів. Ви також могли б використати .head(5)
з ascending=False
, щоб отримати той самий результат.
2.
Отримання найкращих виконавців: хочемо знайти найуспішніші клуби в кожному сезоні за абсолютною кількістю перемог? В SQL ми б використали один з наступних методів ранжування:
SELECT *,
ROW_NUMBER() OVER (PARTITION BY season ORDER BY wins DESC) as row_num,
RANK() OVER (PARTITION BY season ORDER BY wins DESC) as rank,
PERCENT_RANK() OVER (PARTITION BY season ORDER BY wins DESC) as percent_rank,
DENSE_RANK() OVER (PARTITION BY season ORDER BY wins DESC) as dense_rank
FROM club_games_agg
Перед тим як відтворювати це в Pandas, давайте спершу підготуємо таблицю з кількістю перемог за сезон для кожного клубу:
Підготовка даних — перемоги клубів та коефіцієнт перемог для кожного сезону
Pandas має метод .rank()
, який є аналогом SQL-функцій, він підтримує параметри, такі як 'min'
, 'dense'
або 'first'
, для імітації SQL-функцій ранжування. Для ранжування на основі відсотків, подібного до SQL-функції PERCENT_RANK
, можна використовувати параметр pct=True
. Нижче ви можете побачити приклад отримання всіх 4 методів ранжування за допомогою .rank()
:
# Обчислюємо різні типи ранжування для перемог в кожному сезоні
club_games_agg = club_games_agg.sort_values(
["season", "wins"], ascending=[True, False]
# Сортуємо по сезону за зростанням, і по перемогах за спаданням
)
club_games_agg = club_games_agg.assign(
row_number=lambda x: x.groupby("season")["wins"].rank(
method="first", ascending=False
),
rank=lambda x: x.groupby("season")["wins"].rank(
method="min", ascending=False
),
dense_rank=lambda x: x.groupby("season")["wins"].rank(
method="dense", ascending=False
),
percent_rank=lambda x: x.groupby("season")["wins"].rank(
method="min", pct=True, ascending=False
),
)
Кожна функція ранжування має своє призначення в тому, як вона обробляє послідовні значення та рівні. Можна уявити це як різні способи ранжування спортсменів після гонки:
ROW_NUMBER
(.rank(method="first")
) надає унікальні номери незалежно від рівних результатів — як надання номерів учасників перед початком гонки, на основі початкового порядку даних. Завжди буде 1, 2, 3, 4 без повторюваних значень.RANK
(.rank(method="min")
) враховує рівні результати, але залишає прогалини в послідовності — подібно до того, як вручаються медалі на Олімпійських іграх. Якщо два бігуни поділили друге місце, обидва отримують 2, але наступний бігун отримує 4, що відображає їх фактичне місце (1, 2, 2, 4).DENSE_RANK
(.rank(method="dense")
) також дозволяє рівні, але зберігає компактну послідовність. Якщо два учасники поділили друге місце, наступний учасник все одно отримає третє місце (1, 2, 2, 3).PERCENT_RANK
(.rank(method="min", pct=True)
) показує відносне положення на шкалі від 0 до 1. Замість абсолютних позицій, він показує, де кожне значення знаходиться в загальному розподілі.
Найкращі футбольні клуби сезону 2024–2025, за кількістю перемог у матчах
Оглядаючи поточні результати сезону, можна побачити, як різні методи ранжування виконують свою роль. Найпростіший випадок — лідер — Ліверпуль, чия неймовірна кількість перемог (24) і коефіцієнт перемог (80%) робить його лідером за будь-якою методологією. Ситуація з 2-м та 3-м місцями також однозначна, тоді як 4-те місце розділяють Атлетико, Барселона та Зеніт, кожен з яких має по 20 перемог. Наступна група команд з 19 перемогами ще більше виявляє різницю в обробці рівних результатів: dense_rank
продовжує з 5-м місцем для наступної групи, тоді як rank
переходить на 7. Кожен метод має свої специфічні завдання: row_number
для унікальної ідентифікації, rank
для традиційного ранжування, dense_rank
щоб уникнути прогалин і percent_rank
для аналізу розподілу.
Попередні та наступні значення (LAG/LEAD)
Одним з найпоширеніших випадків використання віконних функцій в SQL є доступ до попередніх або наступних значень у послідовності за допомогою LAG
та LEAD
.
У pandas метод .shift()
забезпечує точно таку ж функціональність.
Для демонстрації цього методу давайте розглянемо практичний приклад аналізу серій перемог у футбольних матчах — як часто команда перемагає, якщо попередня гра була виграна?
Підготовка даних — всі матчі кожного клубу
В SQL ми б отримали результати попередніх ігор за допомогою наступної віконної функції:
SELECT
a.*
, LAG(is_win) OVER (ORDER BY date ASC) as prev_game_win
, LAG(is_win, 2) OVER (ORDER BY date ASC) as game_before_last_win
, LEAD(is_win) OVER (ORDER BY date ASC) as next_game_win
FROM club_games as a
Щоб відтворити це в Python, pandas пропонує метод .shift(n)
, який повертає Series того ж розміру, що й вхід, з пересунутими значеннями вгору або вниз в залежності від параметра, який ми передаємо:
- Позитивні числа (наприклад,
shift(1)
) працюють якLAG(1)
, пересуваючи значення стовпця вперед, додаючи NaN на початку - Негативні числа (наприклад,
shift(-1)
) працюють якLEAD(1)
, пересуваючи значення назад, додаючи NaN в кінці - За замовчуванням
fill_value
— NaN, але ми можемо вказати інше значення
Ключова деталь: .shift()
передбачає, що дані вже відсортовані в бажаному порядку. При роботі з даними часового ряду завжди спочатку сортуйте за датою:
# сортуємо дані спочатку
club_games.sort_values('date', ascending=True, inplace=True)
# LAG(1)
club_games['prev_game_win'] = club_games.groupby('club_id').is_win.shift(1)
# LEAD(1)
club_games['next_game_win'] = club_games.groupby('club_id').is_win.shift(-1)
Тепер давайте порівняємо середній коефіцієнт перемог у залежності від результатів попередньої гри:
Клуби, які виграли попередню гру, мають на 4 процентних пункти більше шансів виграти наступну (42% проти 38%)
Аналіз понад 140 000 матчів показує, що хоча імпульс (попередній успіх впливає на наступну гру) існує, його ефект є помірним:
- Команди, що програли попередній матч (85 419 ігор), виграють 38% наступних матчів
- Команди, що виграли попередній матч (56 709 ігор), виграють 42% наступних матчів
Ця різниця в 4% у ймовірності перемоги підтверджує вплив імпульсу, одночасно нагадуючи, що футбол залишається переважно непередбачуваним. Попередні результати мають значення, але це лише один з багатьох факторів, що визначають результати матчів.
Примітка щодо стилю коду: Метод ланцюжка в Pandas
Протягом цієї статті ви, ймовірно, помітили такий шаблон у коді:
result = (
df
.groupby('column')
.agg(metric=('value', 'mean'))
.sort_values('metric', ascending=False)
.round(2)
)
Цей стиль коду pandas, відомий як метод ланцюжка, має кілька ключових переваг:
- Читабельність, оскільки кожна операція знаходиться на окремому рядку, що дозволяє легко слідкувати за потоком трансформацій даних
- Легкість у підтримці, оскільки додавання або видалення кроків здійснюється так само легко, як вставка або видалення рядка
Порівняємо це з підходом у один рядок:
# Менш читабельно, важче змінювати
result = df.groupby('column').agg(metric=('value', 'mean')).sort_values('metric', ascending=False).round(2)
Метод ланцюжка в pandas покращує читабельність коду, встановлюючи чіткий потік трансформацій даних. Хоча він добре працює для простих аналізів, слід враховувати, що він може впливати на продуктивність, створюючи проміжні копії, і може ускладнити налагодження.
Для великих наборів даних або складних трансформацій рекомендується розділяти операції на окремі кроки.
Щоб детальніше ознайомитись з методом ланцюжка, рекомендую ці статті: Modern Pandas (Part 2): Method Chaining та Chaining Pandas Operations: Strengths and Limitations, які дають більш детальний огляд з точки зору продуктивності.
Висновок
У цьому посібнику ми розглянули, як pandas можна використовувати для проведення складного аналізу даних з чистим і легким для читання кодом, використовуючи концепції, знайомі тим, хто має досвід роботи з SQL. Ми продемонстрували інструменти pandas, які виходять за межі базових вступних посібників і можуть значно спростити ваш аналіз, заощаджуючи час.
Основні інструменти та концепції, що були розглянуті:
1. .query()
забезпечує більш чистий синтаксис фільтрації порівняно з булевою індексацією
2. Іменовані агрегації з .agg
роблять операції з групами більш зрозумілими та підтримуваними
3. .merge_asof
дозволяє ефективно виконувати злиття за часом
4. Операції в стилі вікон з .transform
, .rank
і .shift
пропонують гнучкі варіанти для трансформації даних, ранжування та аналізу часового ряду
5. Метод ланцюжка покращує читабельність і підтримку коду
Наш аналіз футболу також розкрив цікаві висновки:
1. Тренд зростання кількості голів спостерігається у всіх змаганнях
2. Перевага домашньої команди поступово зменшується у сучасному європейському футболі
3. Ринкова вартість команди корелює з її результатами, але не є визначальним фактором (навіть різниця в ринковій оцінці в 6 разів не гарантує перемогу)
4. Результати попередніх матчів мають помірний вплив на результати наступних
Не забувайте ознайомитися з повним блокнотом на GitHub! Крива навчання для pandas може бути крутою, але різниця між ним і SQL є відносно невеликою. Насправді, pandas може ефективно доповнити ваші існуючі навички SQL. Його універсальність і ефективність маніпуляцій з даними роблять його важливим інструментом для будь-якого професіонала з роботи з даними. Рекомендую практикувати типові задачі SQL за допомогою pandas і застосовувати продемонстровані техніки до ваших власних випадків. З практикою будь-хто може знайти баланс між цими інструментами і значно покращити свої можливості в аналізі даних.
Ви знайшли інші корисні техніки pandas? Поділіться своїми досвідом у коментарях нижче — я хотів би дізнатися нові підходи від спільноти!
Перекладено з: Getting More Out of Pandas: A Practical Guide for SQL Users