Многие пользователи pandas
имеют некоторое представление о SQL, этот материал рассказывает о том, как различные операции SQL могут быть выполнены при помощи библиотеки pandas
.
В большинстве примеров к материалу будет использоваться данные из тестов библиотеки pandas
. Прочитаем данные в DataFrame
под названием tips
и предположим, что имеется таблица базы данных с тем же именем и структурой.
>>> import pandas as pd >>> import numpy as np url = ( "https://raw.githubusercontent.com/pandas-dev" "/pandas/main/pandas/tests/io/data/csv/tips.csv" ) >>> tips = pd.read_csv(url) >>> tips.head() # total_bill tip sex smoker day time size # 0 16.99 1.01 Female No Sun Dinner 2 # 1 10.34 1.66 Male No Sun Dinner 3 # 2 21.01 3.50 Male No Sun Dinner 3 # 3 23.68 3.31 Male No Sun Dinner 2 # 4 24.59 3.61 Female No Sun Dinner 4
Копии данных по сравнению с операциями на месте
Большинство операций
pandas
возвращают копииSeries
/DataFrame
. Чтобы изменения "вступили в силу", нужно либо присвоить новую переменную или перезаписать исходныйSeries
/DataFrame
:# ! Абстрактный код # новая переменная sorted_df = df.sort_values("col1") # перезапись исходного `DataFrame` df = df.sort_values("col1")Некоторые методы имеют аргумент
inplace=True
илиcopy=False
, напримерdf.replace(5, inplace=True)
. Активно обсуждается вопрос об удаленииinplace
иcopy
для большинства методов, за исключением небольшого подмножества методов.
pandas
с SQL SELECT
;pandas
с SQL WHERE
;pandas
с SQL GROUP BY
;pandas
с SQL JOIN
;pandas
с SQL INNER JOIN
;pandas
с SQL LEFT OUTER JOIN
;pandas
с SQL RIGHT OUTER JOIN
;pandas
с SQL FULL OUTER JOIN
;pandas
с SQL UNION ALL
;n
строк в группе;pandas
с SQL UPDATE
;pandas
с SQL DELETE
;SELECT
В SQL выбор данных выполняется с помощью перечисления (через запятую) столбцов таблицы, которые требуется отобрать (или *
для выбора всех столбцов):
SELECT total_bill, tip, smoker, time FROM tips;
В pandas
выбор столбцов осуществляется путем передачи списка имен столбцов в DataFrame
:
>>> tips[["total_bill", "tip", "smoker", "time"]] # total_bill tip smoker time # 0 16.99 1.01 No Dinner # 1 10.34 1.66 No Dinner # 2 21.01 3.50 No Dinner # 3 23.68 3.31 No Dinner # 4 24.59 3.61 No Dinner # .. ... ... ... ... # 239 29.03 5.92 No Dinner # 240 27.18 2.00 Yes Dinner # 241 22.67 2.00 Yes Dinner # 242 17.82 1.75 No Dinner # 243 18.78 3.00 No Dinner # # [244 rows x 4 columns]
Вызов DataFrame
без списка имен столбцов отобразит все столбцы (аналогично *
в SQL).
В SQL можно добавить вычисляемый столбец:
SELECT *, tip/total_bill as tip_rate FROM tips;
С помощью pandas
можно использовать метод DataFrame.assign()
для добавления нового столбца:
>>> tips.assign(tip_rate=tips["tip"] / tips["total_bill"]) # total_bill tip sex smoker day time size tip_rate # 0 16.99 1.01 Female No Sun Dinner 2 0.059447 # 1 10.34 1.66 Male No Sun Dinner 3 0.160542 # 2 21.01 3.50 Male No Sun Dinner 3 0.166587 # 3 23.68 3.31 Male No Sun Dinner 2 0.139780 # 4 24.59 3.61 Female No Sun Dinner 4 0.146808 # .. ... ... ... ... ... ... ... ... # 239 29.03 5.92 Male No Sat Dinner 3 0.203927 # 240 27.18 2.00 Female Yes Sat Dinner 2 0.073584 # 241 22.67 2.00 Male Yes Sat Dinner 2 0.088222 # 242 17.82 1.75 Male No Sat Dinner 2 0.098204 # 243 18.78 3.00 Female No Thur Dinner 2 0.159744 # # [244 rows x 8 columns]
WHERE
Фильтрация в SQL осуществляется с помощью предложения WHERE
.
SELECT * FROM tips WHERE time = 'Dinner';
DataFrame
можно фильтровать несколькими способами; наиболее интуитивным из них является использование "логической индексации".
>>> tips[tips["total_bill"] > 10].head() # total_bill tip sex smoker day time size # 0 16.99 1.01 Female No Sun Dinner 2 # 1 10.34 1.66 Male No Sun Dinner 3 # 2 21.01 3.50 Male No Sun Dinner 3 # 3 23.68 3.31 Male No Sun Dinner 2 # 4 24.59 3.61 Female No Sun Dinner 4
Приведенный выше оператор просто передает серию объектов True
/False
в DataFrame
, возвращая все строки с True
. Смотрите подробнее, как работает логическая индексация:
Как и в случае с операторами OR
и AND
в SQL, в DataFrame
можно передать несколько условий с помощью |
(ИЛИ) и &
(И).
Условие AND
в SQL:
SELECT * FROM tips WHERE time = 'Dinner' AND tip > 5.00;
Условие AND
в pandas
:
>>> tips[(tips["time"] == "Dinner") & (tips["tip"] > 5.00)] # total_bill tip sex smoker day time size # 23 39.42 7.58 Male No Sat Dinner 4 # 44 30.40 5.60 Male No Sun Dinner 4 # 47 32.40 6.00 Male No Sun Dinner 4 # 52 34.81 5.20 Female No Sun Dinner 4 # 59 48.27 6.73 Male No Sat Dinner 4 # 116 29.93 5.07 Male No Sun Dinner 4 # 155 29.85 5.14 Female No Sun Dinner 5 # 170 50.81 10.00 Male Yes Sat Dinner 3 # 172 7.25 5.15 Male Yes Sun Dinner 2 # 181 23.33 5.65 Male Yes Sun Dinner 2 # 183 23.17 6.50 Male Yes Sun Dinner 4 # 211 25.89 5.16 Male Yes Sat Dinner 4 # 212 48.33 9.00 Male No Sat Dinner 4 # 214 28.17 6.50 Female Yes Sat Dinner 3 # 239 29.03 5.92 Male No Sat Dinner 3
Условие OR
в SQL:
SELECT * FROM tips WHERE size >= 5 OR total_bill > 45;
Условие OR
в pandas
:
>>> tips[(tips["size"] >= 5) | (tips["total_bill"] > 45)] # total_bill tip sex smoker day time size # 59 48.27 6.73 Male No Sat Dinner 4 # 125 29.80 4.20 Female No Thur Lunch 6 # 141 34.30 6.70 Male No Thur Lunch 6 # 142 41.19 5.00 Male No Thur Lunch 5 # 143 27.05 5.00 Female No Thur Lunch 6 # 155 29.85 5.14 Female No Sun Dinner 5 # 156 48.17 5.00 Male No Sun Dinner 6 # 170 50.81 10.00 Male Yes Sat Dinner 3 # 182 45.35 3.50 Male Yes Sun Dinner 3 # 185 20.69 5.00 Male No Sun Dinner 5 # 187 30.46 2.00 Male Yes Sun Dinner 5 # 212 48.33 9.00 Male No Sat Dinner 4 # 216 28.15 3.00 Male Yes Sat Dinner 5
Оператор SQL WHERE ... IN ...
определяет, соответствует ли значение какому-либо значению в списке или подзапросе. Синтаксис оператора WHERE ... IN ...
следующий:
SELECT * FROM tips WHERE 'day' IN ('Sun', 'Sat') and sex='Female'
Аналог оператора SQL WHERE ... IN ...
в pandas
- это метод DataFrame.isin()
. Следовательно приведенный выше SQL запрос в pandas
будет выглядеть следующим образом:
>>> tips[(tips['day'].isin(['Sun', 'Sat'])) & (tips['sex'] == 'Female')] # total_bill tip sex smoker day time size # 0 16.99 1.01 Female No Sun Dinner 2 # 4 24.59 3.61 Female No Sun Dinner 4 # 11 35.26 5.00 Female No Sun Dinner 4 # 14 14.83 3.02 Female No Sun Dinner 2 # 16 10.33 1.67 Female No Sun Dinner 3 # .. ... ... ... ... ... ... ... # 215 12.90 1.10 Female Yes Sat Dinner 2 # 219 30.14 3.09 Female Yes Sat Dinner 4 # 229 22.12 2.88 Female Yes Sat Dinner 2 # 238 35.83 4.67 Female No Sat Dinner 3 # 240 27.18 2.00 Female Yes Sat Dinner 2
Проверка NULL
выполняется с помощью методов .notna()
и .isna()
. Для демонстрации работы методов создадим новый DataFrame
с пропущенными значениями:
frame = pd.DataFrame( {"col1": ["A", "B", np.nan, "C", "D"], "col2": ["F", np.nan, "G", "H", "I"]} ) >>> frame # col1 col2 # 0 A F # 1 B NaN # 2 NaN G # 3 C H # 4 D I
Предположим, что есть таблица той же структуры, что и DataFrame
по имени frame
. В SQL можно отобрать только записи, где col2
имеют значение IS NULL
, с помощью следующего запроса:
SELECT * FROM frame WHERE col2 IS NULL;
В pandas
это достигается методом DataFrame.isna()
>>> frame[frame["col2"].isna()] # col1 col2 # 1 B NaN
Получение элементов, где col1
НЕ РАВНО NULL
, может быть выполнено:
SELECT * FROM frame WHERE col1 IS NOT NULL;
В pandas
это достигается методом DataFrame.notna()
>>> frame[frame["col1"].notna()] # col1 col2 # 0 A F # 1 B NaN # 3 C H # 4 D I
GROUP BY
В pandas
операции SQL GROUP BY
выполняются с помощью метода groupby()
с аналогичным названием. Групповые операции обычно относится к процессу, в котором необходимо разделить набор данных на группы, применить какую-либо функцию (обычно агрегирование), а затем объединить группы вместе.
Распространенной операцией SQL является получение количества записей в каждой группе в наборе данных. Например, запрос, выдающий общее количество записей, в зависимости от пола sex
:
SELECT sex, count(*) FROM tips GROUP BY sex; /* Female 87 Male 157 */
Эквивалентом будет следующий вызов:
>>> tips.groupby("sex").size() # sex # Female 87 # Male 157 # dtype: int64
Обратите внимание, что в коде с pandas
используется .size()
, а не .count()
. Это связано с тем, что метод .count()
применяет функцию к каждому столбцу, возвращая количество записей NOT NULL
в каждом столбце.
>>> tips.groupby("sex").count() # total_bill tip smoker day time size # sex # Female 87 87 87 87 87 87 # Male 157 157 157 157 157 157
В качестве альтернативы, можно было бы применить метод .count()
к отдельному столбцу:
>>> tips.groupby("sex")["total_bill"].count() # sex # Female 87 # Male 157 # Name: total_bill, dtype: int64
Кроме того, можно применять несколько агрегатных функций одновременно. Например, предположим, что необходимо увидеть, как сумма tip
различается в зависимости от дня недели. Метод DataFrame.agg()
позволяет передать в DataFrame
словарь с функциями, которые нужно применять к конкретным столбцам.
В SQL
:
SELECT day, AVG(tip), COUNT(*) FROM tips GROUP BY day; /* Fri 2.734737 19 Sat 2.993103 87 Sun 3.255132 76 Thu 2.771452 62 */
В pandas
:
>>> tips.groupby("day").agg({"tip": "mean", "day": "size"}) # tip day # day # Fri 2.734737 19 # Sat 2.993103 87 # Sun 3.255132 76 # Thur 2.771452 62
Группировка по более чем по одному столбцу осуществляется путем передачи списка столбцов в метод DataFrame.groupby()
.
В SQL
:
SELECT smoker, day, COUNT(*), AVG(tip) FROM tips GROUP BY smoker, day; /* smoker day No Fri 4 2.812500 Sat 45 3.102889 Sun 57 3.167895 Thu 45 2.673778 Yes Fri 15 2.714000 Sat 42 2.875476 Sun 19 3.516842 Thu 17 3.030000 */
В pandas
:
>>> tips.groupby(["smoker", "day"]).agg({"tip": ["size", "mean"]}) # tip # size mean # smoker day # No Fri 4 2.812500 # Sat 45 3.102889 # Sun 57 3.167895 # Thur 45 2.673778 # Yes Fri 15 2.714000 # Sat 42 2.875476 # Sun 19 3.516842 # Thur 17 3.030000
JOIN
JOIN могут выполняться с помощью методов DataFrame.join()
или DataFrame.merge()
. По умолчанию DataFrame.join()
объединяет DataFrames
по их индексам. Каждый метод имеет параметры, позволяющие указать тип выполняемого соединения (ЛЕВОЕ, ПРАВОЕ, ВНУТРЕННЕЕ, ПОЛНОЕ) или столбцы для объединения (имена столбцов или индексы).
Предупреждение. Если оба ключевых столбца содержат строки, где ключ имеет нулевое значение, то эти строки будут сопоставлены друг с другом. Это отличается от обычного поведения соединения в SQL и может привести к неожиданным результатам.
Предположим, что есть две таблицы в БД с тем же именем и структурой, что и у DataFrame
созданных ниже.
>>> df1 = pd.DataFrame({"key": ["A", "B", "C", "D"], "value": np.random.randn(4)}) >>> df2 = pd.DataFrame({"key": ["B", "D", "D", "E"], "value": np.random.randn(4)})
INNER JOIN
SELECT * FROM df1 INNER JOIN df2 ON df1.key = df2.key;
Метод DataFrame.merge()
по умолчанию выполняет INNER JOIN
.
>>> pd.merge(df1, df2, on="key") # key value_x value_y # 0 B 0.813688 1.352346 # 1 D 0.590218 -0.841098 # 2 D 0.590218 1.331496
Метод DataFrame.merge()
также предлагает параметры для случаев, когда необходимо соединить один столбец DataFrame
с индексом другого DataFrame
.
>>> indexed_df2 = df2.set_index("key") >>> pd.merge(df1, indexed_df2, left_on="key", right_index=True) # key value_x value_y # 1 B 0.813688 1.352346 # 3 D 0.590218 -0.841098 # 3 D 0.590218 1.331496
LEFT OUTER JOIN
Отображаем все записи из df1
.
SELECT * FROM df1 LEFT OUTER JOIN df2 ON df1.key = df2.key;
Эквивалент в pandas
:
>>> pd.merge(df1, df2, on="key", how="left") # key value_x value_y # 0 A -0.179623 NaN # 1 B 0.813688 1.352346 # 2 C -0.510149 NaN # 3 D 0.590218 -0.841098 # 4 D 0.590218 1.331496
RIGHT OUTER JOIN
Все записи из df2
.
SELECT * FROM df1 RIGHT OUTER JOIN df2 ON df1.key = df2.key;
Эквивалент в pandas
:
>>> pd.merge(df1, df2, on="key", how="right") # key value_x value_y # 0 B 0.813688 1.352346 # 1 D 0.590218 -0.841098 # 2 D 0.590218 1.331496 # 3 E NaN -1.630031
FULL OUTER JOIN
Библиотека pandas
также допускает FULL OUTER JOIN
, которые показывают оба набора данных, независимо от того, находят ли совпадения объединяемые столбцы.
SELECT * FROM df1 FULL OUTER JOIN df2 ON df1.key = df2.key;
Эквивалент в pandas
:
>>> pd.merge(df1, df2, on="key", how="outer") # key value_x value_y # 0 A -0.179623 NaN # 1 B 0.813688 1.352346 # 2 C -0.510149 NaN # 3 D 0.590218 -0.841098 # 4 D 0.590218 1.331496 # 5 E NaN -1.630031
UNION ALL
SQL операцию UNION ALL
можно выполнить с помощью функции pandas.concat()
.
>>> df1 = pd.DataFrame( ... {"city": ["Chicago", "San Francisco", "New York City"], "rank": range(1, 4)} ... ) >>> df2 = pd.DataFrame( ... {"city": ["Chicago", "Boston", "Los Angeles"], "rank": [1, 4, 5]} ... )
Запрос в SQL:
SELECT city, rank FROM df1 UNION ALL SELECT city, rank FROM df2; /* city rank Chicago 1 San Francisco 2 New York City 3 Chicago 1 Boston 4 Los Angeles 5 */
Эквивалент в pandas
:
>>> pd.concat([df1, df2]) # city rank # 0 Chicago 1 # 1 San Francisco 2 # 2 New York City 3 # 0 Chicago 1 # 1 Boston 4 # 2 Los Angeles 5
UNION
в SQL аналогичен UNION ALL
, однако UNION
удалит повторяющиеся строки.
SELECT city, rank FROM df1 UNION SELECT city, rank FROM df2; -- обратите внимание, что в этот раз есть только одна запись `Chicago` /* city rank Chicago 1 San Francisco 2 New York City 3 Boston 4 Los Angeles 5 */
В pandas
такого поведения можно добиться использовав метод DataFrame.concat()
в сочетании с DataFrame.drop_duplicates()
.
>>> pd.concat([df1, df2]).drop_duplicates() # city rank # 0 Chicago 1 # 1 San Francisco 2 # 2 New York City 3 # 1 Boston 4 # 2 Los Angeles 5
-- MySQL SELECT * FROM tips ORDER BY tip DESC LIMIT 10 OFFSET 5;
Эквивалент в pandas
:
>>> tips.nlargest(10 + 5, columns="tip").tail(10) # total_bill tip sex smoker day time size # 183 23.17 6.50 Male Yes Sun Dinner 4 # 214 28.17 6.50 Female Yes Sat Dinner 3 # 47 32.40 6.00 Male No Sun Dinner 4 # 239 29.03 5.92 Male No Sat Dinner 3 # 88 24.71 5.85 Male No Thur Lunch 2 # 181 23.33 5.65 Male Yes Sun Dinner 2 # 44 30.40 5.60 Male No Sun Dinner 4 # 52 34.81 5.20 Female No Sun Dinner 4 # 85 34.83 5.17 Female No Thur Lunch 4 # 211 25.89 5.16 Male Yes Sat Dinner 4
n
строк в группе-- аналитическая функция Oracle `ROW_NUMBER()` SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER(PARTITION BY day ORDER BY total_bill DESC) AS rn FROM tips t ) WHERE rn < 3 ORDER BY day, rn;
Эквивалент в pandas
:
( tips.assign( rn=tips.sort_values(["total_bill"], ascending=False) .groupby(["day"]) .cumcount() + 1 ) .query("rn < 3") .sort_values(["day", "rn"]) ) # total_bill tip sex smoker day time size rnk # 95 40.17 4.73 Male Yes Fri Dinner 4 1.0 # 90 28.97 3.00 Male Yes Fri Dinner 2 2.0 # 170 50.81 10.00 Male Yes Sat Dinner 3 1.0 # 212 48.33 9.00 Male No Sat Dinner 4 2.0 # 156 48.17 5.00 Male No Sun Dinner 6 1.0 # 182 45.35 3.50 Male Yes Sun Dinner 3 2.0 # 197 43.11 5.00 Female Yes Thur Lunch 4 1.0 # 142 41.19 5.00 Male No Thur Lunch 5 2.0
то же самое, используя функцию DataFrame.rank(method='first')
( tips.assign( rnk=tips.groupby(["day"])["total_bill"].rank( method="first", ascending=False ) ) .query("rnk < 3") .sort_values(["day", "rnk"]) ) # total_bill tip sex smoker day time size rnk # 95 40.17 4.73 Male Yes Fri Dinner 4 1.0 # 90 28.97 3.00 Male Yes Fri Dinner 2 2.0 # 170 50.81 10.00 Male Yes Sat Dinner 3 1.0 # 212 48.33 9.00 Male No Sat Dinner 4 2.0 # 156 48.17 5.00 Male No Sun Dinner 6 1.0 # 182 45.35 3.50 Male Yes Sun Dinner 3 2.0 # 197 43.11 5.00 Female Yes Thur Lunch 4 1.0 # 142 41.19 5.00 Male No Thur Lunch 5 2.0
Найдем tips
с rank
< 3 для каждой группыsex
при tip
< 2.
-- аналитическая функция Oracle `RANK()` SELECT * FROM ( SELECT t.*, RANK() OVER(PARTITION BY sex ORDER BY tip) AS rnk FROM tips t WHERE tip < 2 ) WHERE rnk < 3 ORDER BY sex, rnk;
Обратите внимание, что при использовании функции DataFrame.rank(method='min')
столбец rnk_min
остается неизменным для того же самого tip
(как функция Oracle RANK()
)
( tips[tips["tip"] < 2] .assign(rnk_min=tips.groupby(["sex"])["tip"].rank(method="min")) .query("rnk_min < 3") .sort_values(["sex", "rnk_min"]) ) # total_bill tip sex smoker day time size rnk_min # 67 3.07 1.00 Female Yes Sat Dinner 1 1.0 # 92 5.75 1.00 Female Yes Fri Dinner 2 1.0 # 111 7.25 1.00 Female No Sat Dinner 1 1.0 # 236 12.60 1.00 Male Yes Sat Dinner 2 1.0 # 237 32.83 1.17 Male Yes Sat Dinner 2 2.0
UPDATE
UPDATE tips SET tip = tip*2 WHERE tip < 2;
Эквивалент в pandas
:
>>> tips.loc[tips["tip"] < 2, "tip"] *= 2 >>> tips.head() # total_bill tip sex smoker day time size # 0 16.99 2.02 Female No Sun Dinner 2 # 1 10.34 3.32 Male No Sun Dinner 3 # 2 21.01 3.50 Male No Sun Dinner 3 # 3 23.68 3.31 Male No Sun Dinner 2 # 4 24.59 3.61 Female No Sun Dinner 4
Подробнее о
DataFrame.loc[]
DELETE
DELETE FROM tips WHERE tip > 9;
Эквивалент в pandas
:
# просто пересохраняем `DataFrame` tips = tips.loc[tips["tip"] <= 9]