Оперирование таблицами pandas#

from pathlib import Path
import numpy as np
import pandas as pd


folder = Path("..", "..") / "assets" / "data" / "tables"

Типы столбцов#

Считаем таблицу с планетами и выведем типы данных в её столбце.

planets = pd.read_csv(folder / "planets.csv", index_col="Название")
print(planets.dtypes)
planets
Количество спутников      int64
Масса                   float64
Группа                   object
Кольца                   object
dtype: object
Количество спутников Масса Группа Кольца
Название
Меркурий 0 0.0055 земная группа Нет
Венера 0 0.8150 земная группа Нет
Земля 1 1.0000 земная группа Нет
Марс 2 0.1070 земная группа Нет
Юпитер 62 317.8000 газовый гигант Да
Сатурн 34 95.2000 газовый гигант Да
Уран 27 14.3700 ледяной гигант Да
Нептун 13 17.1500 ледяной гигант Да

Видим, что типы первых столбцов были корректно выведены из таблицы, а вот последние два столбца можно хранить более эффективно.

Во-первых, значение столбца “Кольца” булевого характера. Чтобы сразу считать их сразу как булевые, в методе read_csv можно указать какие значения следует интерпретировать в качестве True, а какие в качестве False, параметрами true_values и false_values.

Tip

По аналогии с true_values и false_values ещё есть параметр na_values, отвечающий за пропущенные значения.

planets = pd.read_csv(folder / "planets.csv", index_col="Название", true_values=["Да"], false_values=["Нет"])
print(planets.dtypes)
planets
Количество спутников      int64
Масса                   float64
Группа                   object
Кольца                     bool
dtype: object
Количество спутников Масса Группа Кольца
Название
Меркурий 0 0.0055 земная группа False
Венера 0 0.8150 земная группа False
Земля 1 1.0000 земная группа False
Марс 2 0.1070 земная группа False
Юпитер 62 317.8000 газовый гигант True
Сатурн 34 95.2000 газовый гигант True
Уран 27 14.3700 ледяной гигант True
Нептун 13 17.1500 ледяной гигант True

Видим, что значения столбца “Кольца” автоматически приняли булевый тип.

Во-вторых, значения столбца “Группа” принимают одно из трех значений, а хранятся в столбце типа object. Переменные такого характера называют категориальными или номинальными. Для их хранения в pandas эффективнее будет воспользоваться специальным категориальным типом данных.

Для начала продемонстрируем работу метода pandas.Series.astype, который позволяет преобразовать тип данных столбца (или таблицы)

planets["Группа"].astype("category")
Название
Меркурий     земная группа
Венера       земная группа
Земля        земная группа
Марс         земная группа
Юпитер      газовый гигант
Сатурн      газовый гигант
Уран        ледяной гигант
Нептун      ледяной гигант
Name: Группа, dtype: category
Categories (3, object): ['газовый гигант', 'земная группа', 'ледяной гигант']

Видим, что метод преобразовал все к типу "category". Эффективнее, конечно, загрузить таблицу сразу с необходимыми типами данных.

dtypes = {
    "Группа": "category",
    "Количество спутников": np.uint8,
    "Кольца": bool,
    "Масса": "float32"
}

planets = pd.read_csv(
    folder / "planets.csv", 
    index_col="Название", 
    true_values=["Да"], 
    false_values=["Нет"],
    dtype=dtypes
)
print(planets.dtypes)
planets
Количество спутников       uint8
Масса                    float32
Группа                  category
Кольца                      bool
dtype: object
Количество спутников Масса Группа Кольца
Название
Меркурий 0 0.005500 земная группа False
Венера 0 0.815000 земная группа False
Земля 1 1.000000 земная группа False
Марс 2 0.107000 земная группа False
Юпитер 62 317.799988 газовый гигант True
Сатурн 34 95.199997 газовый гигант True
Уран 27 14.370000 ледяной гигант True
Нептун 13 17.150000 ледяной гигант True

Названия столбцов#

Иногда может потребоваться переименовать столбцы и метки строк в таблице. Переведем названия столбцов и планет, чтобы продемонстрировать принцип работы метода pd.DataFrame.rename.

columns = {
    "Количество спутников": "number of moons",
    "Масса": "mass",
    "Группа": "group",
    "Кольца": "rings"
}
index = {
    "Меркурий": "Mercury",
    "Венера": "Venus",
    "Земля": "Earth",
    "Марс": "Mars",
    "Юпитер": "jupyter",
    "Сатурн": "Saturn",
    "Уран": "Uranus",
    "Нептун": "Neptune"
} 


planets.rename(
    columns=columns,
    index=index,
    inplace=True
)
planets
number of moons mass group rings
Название
Mercury 0 0.005500 земная группа False
Venus 0 0.815000 земная группа False
Earth 1 1.000000 земная группа False
Mars 2 0.107000 земная группа False
jupyter 62 317.799988 газовый гигант True
Saturn 34 95.199997 газовый гигант True
Uranus 27 14.370000 ледяной гигант True
Neptune 13 17.150000 ледяной гигант True

Т.е. в большинстве случаев удобнее всего указать новые названия столбцов и/или новые метки индекса в виде словаря.

Warning

Обратите внимание, что по умолчанию метод rename не изменяет названия столбцов на месте, а возвращает копию таблицы с переименованными столбцами. Это стандартное поведение для многих методов pandas: в последнее время разработчики pandas стараются избегать мутирующих методов. Однако если талица очень крупная, то её копирование ради совершения простых операций может оказаться накладным. В таком случае полезным может оказаться метод inplace.

Переименовать название индекса таблицы можно методом pandas.Index.rename

planets.index.rename("name", inplace=True)

Пропущенные значения#

Большинство методов pandas разработаны таким образом, чтобы они работали с пропущенными значениями.

import numpy as np

s1 = pd.Series({"a": 1., "b": 2., "c": np.nan})
s2 = pd.Series({"a": -1,}, dtype=pd.Int32Dtype)
s3 = pd.Series(dtype=pd.Float64Dtype)


df = pd.DataFrame({"s1": s1, "s2": s2, "s3": s3})
df
s1 s2 s3
a 1.0 -1 NaN
b 2.0 NaN NaN
c NaN NaN NaN

Например, метод mean вычисляет среднее каждого столбца с численными значениями.

df.mean()
s1    1.5
s2   -1.0
s3    NaN
dtype: float64

Отбрасывание пропущенных значений#

Тем не менее иногда необходимо избавиться от строк или столбцов с пропущенными данными. Для этого удобно использовать метод DataFrame.dropna.

По строкам#

s1 = pd.Series({"a": 1., "b": 2., "c": np.nan})
s2 = pd.Series({"a": -1, "b": -2}, dtype=pd.Int32Dtype)
s3 = pd.Series({"a": 0})


df = pd.DataFrame({"s1": s1, "s2": s2, "s3": s3})
df
s1 s2 s3
a 1.0 -1 0.0
b 2.0 -2 NaN
c NaN NaN NaN

По умолчанию метод DataFrame.dropna отбрасывает все строки, в которых есть хотя бы одно отсутствующее значение.

df.dropna()
s1 s2 s3
a 1.0 -1 0.0

Параметром how можно указать значение all, если необходимо отбросить только те строки, в которых пропущены все значения.

df.dropna(how="all")
s1 s2 s3
a 1.0 -1 0.0
b 2.0 -2 NaN

По столбцам#

df = df.T

df
a b c
s1 1.0 2.0 NaN
s2 -1 -2 NaN
s3 0.0 NaN NaN

Параметром axis можно указать, отбрасывать ли строки или столбцы.

df.dropna(axis="columns")
a
s1 1.0
s2 -1
s3 0.0

Как и в случае со строками можно отбросить только те столбцы, в которых пропущены все значения, указав значение all по параметру how.

df.dropna(axis="columns", how="all")
a b
s1 1.0 2.0
s2 -1 -2
s3 0.0 NaN

Заполнение пропущенных значений#

Иногда стратегия фильтрации пропущенных значений не является оптимальной, например, в случае когда данных не хватает.

boolean = pd.Series({"a": True})
numeric = pd.Series({"b": 1})
color_dtype = pd.CategoricalDtype(categories=["black", "white"])
categorical = pd.Series({"c": "white"}, dtype=color_dtype)

categorical

df = pd.DataFrame({
    "boolean": boolean,
    "numeric": numeric,
    "categorical": categorical
})
df
boolean numeric categorical
a True NaN NaN
b NaN 1.0 NaN
c NaN NaN white

В самом простом случае можно просто заполнить пропущенные значения какими-то константами. Метод df.fillna можно их заполнить. Самый гибкий способ — передать заполнители пропущенных значений для каждого столбца в виде словаря.

df.fillna({
    "boolean": False,
    "numeric": 0,
    "categorical": "black"
})
boolean numeric categorical
a True 0.0 black
b False 1.0 black
c False 0.0 white

Методом DataFrame.interpolate можно заполнить пропущенные значения интерполируя по непропущенным.

s = pd.Series({
    1: 1, 2: np.nan,  4: np.nan, 5: 5, 6: np.nan
})

s.interpolate(method="linear")
1    1.000000
2    2.333333
4    3.666667
5    5.000000
6    5.000000
dtype: float64

apply, map, applymap#

Применить некоторую функцию к всем строкам/столбцам таблицы можно методом DataFrame.apply. Передавать этому методу необходимо такую функцию, рассчитывая, что ей на вход будет подаваться объект pd.Series, который в зависимости от значения параметра axis будет или строкой или столбцом таблицы.

mtcars = pd.read_csv(folder / "mtcars.csv", index_col="model")
mtcars
mpg cyl disp hp drat wt qsec vs am gear carb
model
Mazda RX4 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4
Mazda RX4 Wag 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4
Datsun 710 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1
Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1
Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2
Valiant 18.1 6 225.0 105 2.76 3.460 20.22 1 0 3 1
Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4
Merc 240D 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2
Merc 230 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2
Merc 280 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4
Merc 280C 17.8 6 167.6 123 3.92 3.440 18.90 1 0 4 4
Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3
Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3
Merc 450SLC 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3
Cadillac Fleetwood 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4
Lincoln Continental 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4
Chrysler Imperial 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4
Fiat 128 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1
Honda Civic 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2
Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1
Toyota Corona 21.5 4 120.1 97 3.70 2.465 20.01 1 0 3 1
Dodge Challenger 15.5 8 318.0 150 2.76 3.520 16.87 0 0 3 2
AMC Javelin 15.2 8 304.0 150 3.15 3.435 17.30 0 0 3 2
Camaro Z28 13.3 8 350.0 245 3.73 3.840 15.41 0 0 3 4
Pontiac Firebird 19.2 8 400.0 175 3.08 3.845 17.05 0 0 3 2
Fiat X1-9 27.3 4 79.0 66 4.08 1.935 18.90 1 1 4 1
Porsche 914-2 26.0 4 120.3 91 4.43 2.140 16.70 0 1 5 2
Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.90 1 1 5 2
Ford Pantera L 15.8 8 351.0 264 4.22 3.170 14.50 0 1 5 4
Ferrari Dino 19.7 6 145.0 175 3.62 2.770 15.50 0 1 5 6
Maserati Bora 15.0 8 301.0 335 3.54 3.570 14.60 0 1 5 8
Volvo 142E 21.4 4 121.0 109 4.11 2.780 18.60 1 1 4 2
from matplotlib import pyplot as plt

def f(x: pd.Series):
    fig, ax = plt.subplots(figsize=(1.5, 1))
    if isinstance(x.dtype, float):
        ax.hist(x)
    else:
        vc = x.value_counts()
        ax.bar(vc.index, vc.array)
    ax.set_title(x.name)
    plt.show(fig)
    return x.max() - x.min()

mtcars.apply(f, axis="rows").head()
../../_images/operations_38_0.png ../../_images/operations_38_1.png ../../_images/operations_38_2.png ../../_images/operations_38_3.png ../../_images/operations_38_4.png ../../_images/operations_38_5.png ../../_images/operations_38_6.png ../../_images/operations_38_7.png ../../_images/operations_38_8.png ../../_images/operations_38_9.png ../../_images/operations_38_10.png
mpg      23.50
cyl       4.00
disp    400.90
hp      283.00
drat      2.17
dtype: float64

Метод Series.map позволяет применить функцию к каждому значению столбца. Чтобы продемонстрировать принцип работы этого метода, извлечем из столбца brand марку и модель автомобиля в разные столбцы.

Для этого определим функции, которые в качестве аргумента принимают строку из столбца brand и возвращают марку или модель автомобиля из неё. В данной таблице, марка автомобиля всегда встречается до первого пробела, а модель автомобиля — все после первого пробела.

def get_brand(x: pd.Series):
    return x.split(" ")[0]

def get_model(x: pd.Series):
    b, *m = x.split(" ") # все после первого пробела попадает в переменную m
    return " ".join(m)

x = "Mazda RX4 Wag"
print(f"{x} -> {get_brand(x)} and {get_model(x)}")
Mazda RX4 Wag -> Mazda and RX4 Wag

Определив эти функции применим их к столбцу brand. Т.к. brand — индекс, то создадим из него pd.Series, применим определенные функции методом map и запишем результаты в таблицу в качестве новых столбцов.

fullname = pd.Series(mtcars.index, index=mtcars.index)
mtcars.index.rename("car", inplace=True)
mtcars["brand"] = fullname.map(get_brand)
mtcars["model"] = fullname.map(get_model)
mtcars
mpg cyl disp hp drat wt qsec vs am gear carb brand model
car
Mazda RX4 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4 Mazda RX4
Mazda RX4 Wag 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4 Mazda RX4 Wag
Datsun 710 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1 Datsun 710
Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1 Hornet 4 Drive
Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2 Hornet Sportabout
Valiant 18.1 6 225.0 105 2.76 3.460 20.22 1 0 3 1 Valiant
Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4 Duster 360
Merc 240D 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2 Merc 240D
Merc 230 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2 Merc 230
Merc 280 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4 Merc 280
Merc 280C 17.8 6 167.6 123 3.92 3.440 18.90 1 0 4 4 Merc 280C
Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3 Merc 450SE
Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3 Merc 450SL
Merc 450SLC 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3 Merc 450SLC
Cadillac Fleetwood 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4 Cadillac Fleetwood
Lincoln Continental 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4 Lincoln Continental
Chrysler Imperial 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4 Chrysler Imperial
Fiat 128 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1 Fiat 128
Honda Civic 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2 Honda Civic
Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1 Toyota Corolla
Toyota Corona 21.5 4 120.1 97 3.70 2.465 20.01 1 0 3 1 Toyota Corona
Dodge Challenger 15.5 8 318.0 150 2.76 3.520 16.87 0 0 3 2 Dodge Challenger
AMC Javelin 15.2 8 304.0 150 3.15 3.435 17.30 0 0 3 2 AMC Javelin
Camaro Z28 13.3 8 350.0 245 3.73 3.840 15.41 0 0 3 4 Camaro Z28
Pontiac Firebird 19.2 8 400.0 175 3.08 3.845 17.05 0 0 3 2 Pontiac Firebird
Fiat X1-9 27.3 4 79.0 66 4.08 1.935 18.90 1 1 4 1 Fiat X1-9
Porsche 914-2 26.0 4 120.3 91 4.43 2.140 16.70 0 1 5 2 Porsche 914-2
Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.90 1 1 5 2 Lotus Europa
Ford Pantera L 15.8 8 351.0 264 4.22 3.170 14.50 0 1 5 4 Ford Pantera L
Ferrari Dino 19.7 6 145.0 175 3.62 2.770 15.50 0 1 5 6 Ferrari Dino
Maserati Bora 15.0 8 301.0 335 3.54 3.570 14.60 0 1 5 8 Maserati Bora
Volvo 142E 21.4 4 121.0 109 4.11 2.780 18.60 1 1 4 2 Volvo 142E

Вместо функции, методу map можно передать словарь.

translation = {
    "земная группа": "terrestrial group",
    "газовый гигант": "gas giant",
    "ледяной гигант": "ice giant"
    }

planets["group"] = planets["group"].map(translation)
planets
number of moons mass group rings
name
Mercury 0 0.005500 terrestrial group False
Venus 0 0.815000 terrestrial group False
Earth 1 1.000000 terrestrial group False
Mars 2 0.107000 terrestrial group False
jupyter 62 317.799988 gas giant True
Saturn 34 95.199997 gas giant True
Uranus 27 14.370000 ice giant True
Neptune 13 17.150000 ice giant True

Метод DataFrame.applymap применяет функцию к кажому значению в таблице.

Группировка по значениям столбца#

Часто необходимо сгруппировать строки таблицы по какому-то принципу и с каждой из групп проделать какие-то операции.

../../_images/groupby.svg

За группировку в pandas отвечает метод DataFrame.groupby. В качестве аргумента ей можно передать имя столбца, по значениям которого необходима группировка (можно также передать несколько столбцов в списке).

from pprint import pprint

grouped = planets.groupby("group")
pprint(dict(grouped.groups))
{'gas giant': Index(['jupyter', 'Saturn'], dtype='object', name='name'),
 'ice giant': Index(['Uranus', 'Neptune'], dtype='object', name='name'),
 'terrestrial group': Index(['Mercury', 'Venus', 'Earth', 'Mars'], dtype='object', name='name')}

Каждую группу можно рассматривать как мини-таблицу.

grouped.get_group("terrestrial group")
number of moons mass group rings
name
Mercury 0 0.0055 terrestrial group False
Venus 0 0.8150 terrestrial group False
Earth 1 1.0000 terrestrial group False
Mars 2 0.1070 terrestrial group False

Часто таблицу группируют по значениям одного столбца, чтобы потом посчитать какие-нибудь агрегирующие функцию к значениям других столбцов. Сделать это можно методом DataFrameGroupBy.aggregate.

grouped["number of moons"].aggregate([min, np.mean, np.max, "median", "count"])
min mean amax median count
group
gas giant 34 48.00 62 48.0 2
terrestrial group 0 0.75 2 0.5 4
ice giant 13 20.00 27 20.0 2

Гораздо чаще в отдельном виде объект группировки не выделяют.

mtcars.groupby("brand")["mpg"].aggregate("count")
brand
AMC         1
Cadillac    1
Camaro      1
Chrysler    1
Datsun      1
Dodge       1
Duster      1
Ferrari     1
Fiat        2
Ford        1
Honda       1
Hornet      2
Lincoln     1
Lotus       1
Maserati    1
Mazda       2
Merc        7
Pontiac     1
Porsche     1
Toyota      2
Valiant     1
Volvo       1
Name: mpg, dtype: int64