Сравнение с SQL

Многие потенциальные пользователи pandas имеют некоторое представление о SQL. На этой странице приведены примеры того, как различные операции SQL будут выполняться в pandas.

Если вы новичок в pandas, вы можете сначала прочитать 10 Minutes to pandas, чтобы ознакомиться с библиотекой.

Как обычно, импортируем pandas и NumPy следующим образом:

In [1]: import pandas as pd

In [2]: import numpy as np

В большинстве примеров будет использоваться набор данных tips, найденный в тестах pandas. Мы прочитаем данные в DataFrame под названием tips и предположим, что у нас есть таблица базы данных с таким же именем и структурой.

In [3]: url = (
   ...:     "https://raw.github.com/pandas-dev"
   ...:     "/pandas/main/pandas/tests/io/data/csv/tips.csv"
   ...: )
   ...: 

In [4]: tips = pd.read_csv(url)

In [5]: tips
Out[5]: 
     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
..          ...   ...     ...    ...   ...     ...   ...
239       29.03  5.92    Male     No   Sat  Dinner     3
240       27.18  2.00  Female    Yes   Sat  Dinner     2
241       22.67  2.00    Male    Yes   Sat  Dinner     2
242       17.82  1.75    Male     No   Sat  Dinner     2
243       18.78  3.00  Female     No  Thur  Dinner     2

[244 rows x 7 columns]

Копии и операции на месте

Большинство операций pandas возвращают копии Series/DataFrame. Чтобы зафиксировать изменения, вам нужно либо назначить новую переменную:

sorted_df = df.sort_values("col1")

Либо перезаписать исходный объект:

df = df.sort_values("col1")

Примечание

Вы можете столкнуться с аргументом inplace=True, доступным для некоторых методов:

df.sort_values("col1", inplace=True)

Мы не рекомендуем его использовать.

SELECT

В SQL выбор осуществляется с помощью списка столбцов, разделенных запятыми, которые вы хотите выбрать (или *, чтобы выбрать все столбцы):

SELECT total_bill, tip, smoker, time
FROM tips;

В pandas выбор столбца выполняется путем передачи списка имен столбцов в ваш DataFrame:

In [6]: tips[["total_bill", "tip", "smoker", "time"]]
Out[6]: 
     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(), чтобы добавить новый столбец в DataFrame:

In [7]: tips.assign(tip_rate=tips["tip"] / tips["total_bill"])
Out[7]: 
     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';

DataFrames можно фильтровать несколькими способами; наиболее интуитивным из них является использование логического индексирования.

In [1]: tips[tips["total_bill"] > 10]
Out[1]: 
     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
..          ...   ...     ...    ...   ...     ...   ...
239       29.03  5.92    Male     No   Sat  Dinner     3
240       27.18  2.00  Female    Yes   Sat  Dinner     2
241       22.67  2.00    Male    Yes   Sat  Dinner     2
242       17.82  1.75    Male     No   Sat  Dinner     2
243       18.78  3.00  Female     No  Thur  Dinner     2

[227 rows x 7 columns]

Вышеприведенный оператор просто передает Series со значениями True/False в DataFrame, возвращая все строки с True.

In [1]: is_dinner = tips["time"] == "Dinner"

In [2]: is_dinner
Out[2]: 
0      True
1      True
2      True
3      True
4      True
       ... 
239    True
240    True
241    True
242    True
243    True
Name: time, Length: 244, dtype: bool

In [3]: is_dinner.value_counts()
Out[3]: 
True     176
False     68
Name: time, dtype: int64

In [4]: tips[is_dinner]
Out[4]: 
     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
..          ...   ...     ...    ...   ...     ...   ...
239       29.03  5.92    Male     No   Sat  Dinner     3
240       27.18  2.00  Female    Yes   Sat  Dinner     2
241       22.67  2.00    Male    Yes   Sat  Dinner     2
242       17.82  1.75    Male     No   Sat  Dinner     2
243       18.78  3.00  Female     No  Thur  Dinner     2

[176 rows x 7 columns]

Как с OR и AND в SQL, можно передать в DataFrame несколько условий с помощью | (OR) и & (AND).

Чаевые на сумму более 5 долларов за ужин:

SELECT *
FROM tips
WHERE time = 'Dinner' AND tip > 5.00;
In [5]: tips[(tips["time"] == "Dinner") & (tips["tip"] > 5.00)]
Out[5]: 
     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

Чаевые от вечеринок, состоящих не менее чем из 5 посетителей ИЛИ общая сумма счета в которых превышает 45 долларов:

SELECT *
FROM tips
WHERE size >= 5 OR total_bill > 45;
In [6]: tips[(tips["size"] >= 5) | (tips["total_bill"] > 45)]
Out[6]: 
     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

Проверка NULL выполняется с помощью методов notna() и isna().

In [7]: frame = pd.DataFrame(
   ...:     {"col1": ["A", "B", np.NaN, "C", "D"], "col2": ["F", np.NaN, "G", "H", "I"]}
   ...: )
   ...: 

In [8]: frame
Out[8]: 
  col1 col2
0    A    F
1    B  NaN
2  NaN    G
3    C    H
4    D    I

Предположим, у нас есть таблица той же структуры, что и наш DataFrame выше. Мы можем видеть только те записи, где в col2 значение IS NULL со следующим запросом:

SELECT *
FROM frame
WHERE col2 IS NULL;
In [9]: frame[frame["col2"].isna()]
Out[9]: 
  col1 col2
1    B  NaN

Получение элементов, где в col2 значение IS NOT NULL, может быть выполнено с помощью notna().

SELECT *
FROM frame
WHERE col1 IS NOT NULL;
In [10]: frame[frame["col1"].notna()]
Out[10]: 
  col1 col2
0    A    F
1    B  NaN
3    C    H
4    D    I

GROUP BY

В pandas операции, подобные GROUP BY в SQL, выполняются с использованием метода с аналогичным названием groupby(). groupby() обычно относится к процессу, в котором мы хотели бы разделить набор данных на группы, применить некоторую функцию (обычно агрегацию), а затем объединить группы вместе.

Обычной операцией SQL будет получение количества записей в каждой группе по всему набору данных. Например, запрос, дающий нам количество чаевых, оставленных посетителями определенного пола:

SELECT sex, count(*)
FROM tips
GROUP BY sex;
/*
Female     87
Male      157
*/

Эквивалент в pandas будет такой:

In [11]: tips.groupby("sex").size()
Out[11]: 
sex
Female     87
Male      157
dtype: int64

Обратите внимание, что в коде pandas мы использовали size(), а не count(). Это связано с тем, что count() применяет функцию к каждому столбцу, возвращая количество записей с ненулевыми значениями в каждом.

In [12]: tips.groupby("sex").count()
Out[12]: 
        total_bill  tip  smoker  day  time  size
sex                                             
Female          87   87      87   87    87    87
Male           157  157     157  157   157   157

В качестве альтернативы мы могли бы применить метод count() к отдельному столбцу:

In [13]: tips.groupby("sex")["total_bill"].count()
Out[13]: 
sex
Female     87
Male      157
Name: total_bill, dtype: int64

Несколько функций можно применять одновременно. Скажем, мы хотели бы увидеть, как сумма чаевых различается в зависимости от дня недели. Метод agg() позволяет вам передать словарь в ваш сгруппированный DataFrame, указав, какие функции применять к определенным столбцам.

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
*/
In [14]: tips.groupby("day").agg({"tip": np.mean, "day": np.size})
Out[14]: 
           tip  day
day                
Fri   2.734737   19
Sat   2.993103   87
Sun   3.255132   76
Thur  2.771452   62

Группировка по нескольким столбцам выполняется путем передачи списка столбцов в метод groupby().

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
*/
In [15]: tips.groupby(["smoker", "day"]).agg({"tip": [np.size, np.mean]})
Out[15]: 
             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 в SQL, можно выполнить с помощью join() или merge(). По умолчанию join() объединяет DataFrame по их индексам. Каждый метод имеет параметры, позволяющие вам указать тип объдинения (LEFT, RIGHT, INNER, FULL) или столбцы для объединения (имена столбцов или их индексы).

Предупреждение

Если оба ключевых столбца содержат строки, в которых ключ является нулевым значением, эти строки будут сопоставляться друг с другом. Это отличается от обычного поведения SQL при объединении и может привести к неожиданным результатам.

In [16]: df1 = pd.DataFrame({"key": ["A", "B", "C", "D"], "value": np.random.randn(4)})

In [17]: df2 = pd.DataFrame({"key": ["B", "D", "D", "E"], "value": np.random.randn(4)})

Предположим, у нас есть две таблицы базы данных с тем же именем и структурой, что и у наших DataFrames.

Теперь давайте рассмотрим различные типы JOIN.

INNER JOIN

SELECT *
FROM df1
INNER JOIN df2
  ON df1.key = df2.key;
# merge performs an INNER JOIN by default
In [18]: pd.merge(df1, df2, on="key")
Out[18]: 
  key   value_x   value_y
0   B -0.282863  1.212112
1   D -1.135632 -0.173215
2   D -1.135632  0.119209

merge() также предлагает параметры для случаев, когда вы хотите соединить один столбец DataFrame с индексом другого DataFrame.

In [19]: indexed_df2 = df2.set_index("key")

In [20]: pd.merge(df1, indexed_df2, left_on="key", right_index=True)
Out[20]: 
  key   value_x   value_y
1   B -0.282863  1.212112
3   D -1.135632 -0.173215
3   D -1.135632  0.119209

LEFT OUTER JOIN

Показать все записи из df1.

SELECT *
FROM df1
LEFT OUTER JOIN df2
  ON df1.key = df2.key;
In [21]: pd.merge(df1, df2, on="key", how="left")
Out[21]: 
  key   value_x   value_y
0   A  0.469112       NaN
1   B -0.282863  1.212112
2   C -1.509059       NaN
3   D -1.135632 -0.173215
4   D -1.135632  0.119209

RIGHT JOIN

Показать все записи из df2.

SELECT *
FROM df1
RIGHT OUTER JOIN df2
  ON df1.key = df2.key;
In [22]: pd.merge(df1, df2, on="key", how="right")
Out[22]: 
  key   value_x   value_y
0   B -0.282863  1.212112
1   D -1.135632 -0.173215
2   D -1.135632  0.119209
3   E       NaN -1.044236

FULL JOIN

pandas также допускает FULL JOIN, при котором отображаются обе стороны набора данных, независимо от того, нашлись ли совпадения для объединяемых столбцов. На момент написания FULL JOIN поддерживается не во всех СУБД (MySQL).

Показать все записи из обеих таблиц.

SELECT *
FROM df1
FULL OUTER JOIN df2
  ON df1.key = df2.key;
In [23]: pd.merge(df1, df2, on="key", how="outer")
Out[23]: 
  key   value_x   value_y
0   A  0.469112       NaN
1   B -0.282863  1.212112
2   C -1.509059       NaN
3   D -1.135632 -0.173215
4   D -1.135632  0.119209
5   E       NaN -1.044236

UNION

UNION ALL можно выполнить в pandas с помощью concat().

In [24]: df1 = pd.DataFrame(
   ....:     {"city": ["Chicago", "San Francisco", "New York City"], "rank": range(1, 4)}
   ....: )
   ....: 

In [25]: df2 = pd.DataFrame(
   ....:     {"city": ["Chicago", "Boston", "Los Angeles"], "rank": [1, 4, 5]}
   ....: )
   ....: 
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
*/
In [26]: pd.concat([df1, df2])
Out[26]: 
            city  rank
0        Chicago     1
1  San Francisco     2
2  New York City     3
0        Chicago     1
1         Boston     4
2    Los Angeles     5

В SQL UNION аналогичен UNION ALL, однако UNION удалит повторяющиеся строки.

SELECT city, rank
FROM df1
UNION
SELECT city, rank
FROM df2;
-- notice that there is only one Chicago record this time
/*
         city  rank
      Chicago     1
San Francisco     2
New York City     3
       Boston     4
  Los Angeles     5
*/

В pandas вы можете использовать concat() в сочетании с drop_duplicates().

In [27]: pd.concat([df1, df2]).drop_duplicates()
Out[27]: 
            city  rank
0        Chicago     1
1  San Francisco     2
2  New York City     3
1         Boston     4
2    Los Angeles     5

LIMIT

SELECT * FROM tips
LIMIT 10;
In [28]: tips.head(10)
Out[28]: 
   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
5       25.29  4.71    Male     No  Sun  Dinner     4
6        8.77  2.00    Male     No  Sun  Dinner     2
7       26.88  3.12    Male     No  Sun  Dinner     4
8       15.04  1.96    Male     No  Sun  Dinner     2
9       14.78  3.23    Male     No  Sun  Dinner     2

Эквиваленты pandas для некоторых аналитических и агрегирующих функций SQL

Верхние n строк со смещением

-- MySQL
SELECT * FROM tips
ORDER BY tip DESC
LIMIT 10 OFFSET 5;
In [29]: tips.nlargest(10 + 5, columns="tip").tail(10)
Out[29]: 
     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's ROW_NUMBER() analytic function
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;
In [30]: (
   ....:     tips.assign(
   ....:         rn=tips.sort_values(["total_bill"], ascending=False)
   ....:         .groupby(["day"])
   ....:         .cumcount()
   ....:         + 1
   ....:     )
   ....:     .query("rn < 3")
   ....:     .sort_values(["day", "rn"])
   ....: )
   ....: 
Out[30]: 
     total_bill    tip     sex smoker   day    time  size  rn
95        40.17   4.73    Male    Yes   Fri  Dinner     4   1
90        28.97   3.00    Male    Yes   Fri  Dinner     2   2
170       50.81  10.00    Male    Yes   Sat  Dinner     3   1
212       48.33   9.00    Male     No   Sat  Dinner     4   2
156       48.17   5.00    Male     No   Sun  Dinner     6   1
182       45.35   3.50    Male    Yes   Sun  Dinner     3   2
197       43.11   5.00  Female    Yes  Thur   Lunch     4   1
142       41.19   5.00    Male     No  Thur   Lunch     5   2

То же самое с использованием функции rank(method='first')

In [31]: (
   ....:     tips.assign(
   ....:         rnk=tips.groupby(["day"])["total_bill"].rank(
   ....:             method="first", ascending=False
   ....:         )
   ....:     )
   ....:     .query("rnk < 3")
   ....:     .sort_values(["day", "rnk"])
   ....: )
   ....: 
Out[31]: 
     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
-- Oracle's RANK() analytic function
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;

Давайте найдем чаевые с (rank < 3) для каждой гендерной группы, где (tips < 2). Обратите внимание, что при использовании rank(method='min') функция rnk_min остается неизменной для того же tip (как функция Oracle RANK()).

In [32]: (
   ....:     tips[tips["tip"] < 2]
   ....:     .assign(rnk_min=tips.groupby(["sex"])["tip"].rank(method="min"))
   ....:     .query("rnk_min < 3")
   ....:     .sort_values(["sex", "rnk_min"])
   ....: )
   ....: 
Out[32]: 
     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;
In [33]: tips.loc[tips["tip"] < 2, "tip"] *= 2

DELETE

DELETE FROM tips
WHERE tip > 9;

В pandas мы не удаляем ненужные строки, а оставляем нужные:

In [34]: tips = tips.loc[tips["tip"] <= 9]