REF: Шпаргалка по Pandas


Создание таблиц

В pandas есть два основных объекта: DataFrame (таблица) и Series (последовательность значений, например, один столбец DataFrame). У таблицы есть ключи (индексы) строчек - index и колонок - columns. По умолчанию index целочисленный. Индексы строк не обязаны быть уникальными. При их совпадении, обращение к индексу df.loc[row] вернёт таблицу с несколькими строками.

Принципиальная разница с массивами numpy в том, что колонки pandas DataFrame могут быть различного типа (в том числе экземплярами класса). Таблицу можно транспонировать df.T и в этом смысле особой разницы между строками и колонками нет. Но если колонки различного типа транспонирование превратит все типы в object.

import pandas as pd

df = pd.DataFrame()              # создаём пустую таблицу
print(df.index, df.columns)      # Index( [] )  Index([])

df = pd.DataFrame([1,3,2],       # одна колонка из 3-х чисел с именем A
                  columns=['A']) # индексы строчек по умолчанию [0,1,2]
print(df.index)                  # RangeIndex(start=0,stop=3,step=1)
print(df.columns)                # Index(['A'])

df = pd.DataFrame()              # тоже самое
df['A'] = [1,2,3]                # создаём колонку в пустой таблице

df = pd.DataFrame([[1,2],        # таблица из 2D массива
                   [3,4]])       # первая строка [1,2], вторая [3,4]

Таблицу можно задать при помощи словаря (его ключи - имена колонок). Если у ключа скалярное значение - оно будет продублировано. При задании ключей строчек (index) можно смешивать str, int, Timestamp и т.п.:

cat = pd.Categorical(["a","b","a"])}        # категориальные (не строки!)

df  = pd.DataFrame(
          {"Num":  1,                       #    | Num Name Age Cat
           "Name": ["Jon", "Mia", "Sam"],   #  --------------------
           "Age" : [43,    np.nan, 56],     #  m | 1   Jon  43   a
           "Cat":  cat,                     #  f | 1   Mia  NaN  b
           index=['m', 'f', 0])             #  0 | 1   Sam  56   a

Список словарей - это список строк. Каждый новый ключ порождает колонку:
                                            #    |  b   c   a
pd.DataFrame([{        'b': 1, 'c': 2},     #  ----------------
              {'a': 3, 'b': 4, 'c': 5}])    #  0 |  1   2   NaN
                                            #  1 |  3   4   5
df1 =  df.copy()                            # deepcopy таблицы

Работа с файлами

При загрузке из csv-файла можно (index_col) задать некоторую колонку (по имени или по номеру) как индексную (имена строчек):
df = pd.read_csv("name.csv", index_col="id")
Чтение больших файлов по кускам:
chunksize = 10 ** 6
with pd.read_csv(filename, chunksize=chunksize) as reader:
    for chunk in reader:
        process(chunk)
Сохранение в csv-файл:
df.to_csv("submission.csv", index=False)   # без сохранения индекса

df.to_csv('probs.zip', index=False,        # запаковать в zip-файл
           compression=dict(method='zip', archive_name='probs.csv') )
Если index=False не указывать, в csv добавиться первая безымянная колонка с индексами.

Сводная информация

print('shape:', df.shape)       # вывод формы (rows, cols)
display(df)                     # распечатать (если не последняя команда)

df.head(7)                      # 7 первых строк     (5 по умолчанию)
df.tail(2)                      # 2 последние строки (5 по умолчанию)
df.sample(2)                    # 2 случайные строки

df.describe()                   # статистики по всем колонкам
df.col.describe()               # статистики колонке c именем 'col'
df.describe(include=[bool]))    # только логические колонки (bool,float,object,category)

df.col.mean()                   # среднее значение по колонке 'col'
df.col.unique()                 # список уникальных значений
df.col.value_counts()           # таблица имя значения - число раз в 'col'

df.info()                       # список колонок с типами и непустых данных
df.duplicated().sum()           # число дубликатов строк
df.isna().sum()                 # вывод числа пропусков в каждой колонке
Формат для вывода describe и т.п.:
pd.set_option('display.float_format', lambda x: '%.2f' % x)

Доступ к элементам

Метод at даёт доступ к элементам таблицы, а метод loc - к срезам по индексам (строк и колонок). Тоже самое для iat, iloc, но индексация при помощи номеров строк и колонок (целые числа, срезы подобно numpy). Для loc срезы включают границы, а для iloc срезы понимаются в обычном для Python смысле (верхняя граница не включена).

Свойство at[rowID, colID] быстрее, чем loc[rowID, colID] для одного элемена:
df.at[0,'a'] = 5

Свойство loc позволяет извлечь одну строку, список строк или их срез, а также срез таблицы (index, columns). При этом в срезе последний индекс включается (!):
df.loc[ 0 ]                     # Series    (0-я строка)
df.loc[ 0 ] = 5                 # присвоить всем элементам строки 0 значение 5
df.loc[ [1,3] ]                 # DataFrame с 2-я строками
df.loc[ :2]                     # DataFrame c 3-я строками

df.loc[0,   'a']                # аналогично at, но медленнее
df.loc[1:3, 'a']                # результат - Series (3 элемента)
df.a.loc[1:3]                   # тоже самое (можно и со строковыми индексами)

df.loc[1:3,   ['a','b']]        # DataFrame (срез 3 строки, 2 колонки)
df.loc[[1,3], ['a','b']]        # DataFrame (вырез 2 строки, 2 колонки)

Получение колонок: df['col_name'] - это даёт Series. Тот-же эффект: df.col_name (если колонка уже существует). Можно список имён колонок: df[['a','c']] - тогда это DataFrame, а не Series.

df['a'] = 5          # изменить все значения в колонке 5
df.a    = 5          # тоже самое
df['a'] = [1,2,3]    # элементы колонки (должна совпадать с другими колонками)

Логическая индексация

df.loc[ df.a > 1 ]              # DataFrame в которой значения в кллонке a > 1
df.loc[ (df.a > 1) & (df.a < 5) ]

df.loc[df.c.isin(['aa','bb'])]  # присутствуют значения в колонке 'c'
df.loc[df.isin(['aa','bb'])]    # присутствуют значения в любой колонке

df.loc[lambda df: df.a == 8]    # отобрать строки, удовлетворящие функции

df[df > 0]                      # все элементы большие нуля (остальные NaN)
df[df > 0] = -df

Изменение таблиц

Сортировка:

df = df.sort_values(by="A")                   # сортировки по колонкам
df = df.sort_values(by=["A", "B"])

df = df.sort_index(axis=1, ascending=False)   # сортировка колонок
df = df.sort_index(axis=0)                    # сортировка индексов строк (один типа!)

Переименование колонок и индексов строк:

df.rename(columns = {'points': 'score'})
df.rename(index   = {0: 'firstEntry', 1: 'secondEntry'})

Добавление строк и колонок

Добавляем строку при помощи loc или конкатенацией concat таблиц:
df = pd.DataFrame({'a':[1,2], 'b':[-1,-2]}) #   | a b
                                            # ----------
df.loc[len(df.index)] = [3, -3]             # 0 | 1 -1
                                            # 1 | 2 -2
da = pd.DataFrame([{'a':4, 'b':'-4'}])      # 2 | 3 -3
df = pd.concat([df, da])                    # 3 | 4 -4
df = df.reset_index()                       # сделать снова индексы строк уникальными (!)
Добавление колонки со значением длины строки в колонке "name"
df["len"] = df["name"].map(lambda x: len(x))

Неопределённые ячейки

Некоторые ячейки могут быть неопределёнными (None или np.nan), что при выводе отражается как NaN. Метод isna возвращает DataFrame с логическими значениями (его антипод notna):
df = pd.DataFrame({'a':[1,2], 'b':[3,None]})   #    | a     b
                                               #  ---------------
                                               #  0 | False False
df.isna()                                      #  1 | False True

Можно создавать таблицу с неопределёнными ячейками (для порождения больших таблиц эффективнее создать сначала неопределённую, и затем её заполнять, чем добавлять строки). Значениями в ячейках могут быть объекты (списки и т.п.).

df = pd.DataFrame(index=range(2),              #     | a
                  columns=['a'])               #  --------
df.at[0,'a'] = [1,2]                           #   0 | [1,2]
df                                             #   1 | NaN
Но после сохранения в файл они будут json-строками и после чтения файла их надо будет снова парсить.

df.dropna(how="any")           # выкинуть все строки с пропусками
df.fillna(value=5)             # заполнить одним значением

Вычисления в таблицах

df['c']= df.a + df.b                # создаём новую колонку или меняем существующую

df.a = df.a.map(lambda x: x - 50)   # меняем колонку при помощи функции
count количество не-NA объектов
sum cумма
mean cреднее значение
mad cреднее абсолютное отклонение
median медиана
min минимум
max максимум
mode мода
abs абсолютное значение
prod произведение
std стандартное отклонение
var несмещенная дисперсия
sem стандартная ошибка среднего
skew скошенность (момент 3-го порядка)
kurt эксцесс (момент 4-го порядка)
quantile квантиль (%)
cumsum кумулятивная сумма
cumprod кумулятивное произведение
cummax кумулятивный максимум
cummin кумулятивный минимум

Агрегированная информация

Вместо стандартной describe, можно самостоятельно формировать итоговые таблицы по колонкам (текстовые имена для функций панды df.sum и т.п.), иначе просто имя функции:
def fun(x):
    return sum(x)**0.5

fun.__name__ = 'sqrt'     # можно для вывода любое имя

df.agg(['sum', 'mean', 'std', max, strange])

Группы

df = pd.DataFrame({
    'A': ['a','b','a','b','b'],
    'B': ['0','1','0','1','0'],
    'X': [ 1,  2,  3,  4,  5 ],
    'Y': [ 0,  1,  1,  0,  0 ]
} )
Сгруппируем одинаковые значения в колонке A и пробежимся по всем группам (каждая группа - это DataFrame):
groups = df.groupby("A")          # val=a
                                  #   | A  B  X  Y
for val_A, df_A in groups:        # --------------
    print("val=",val_A)           # 0 | a  0  1  0
    display(df_A)                 # 2 | a  0  3  1
                                  #...
Получим сумму по всем числовым колонкам в каждой группе (индекс получил имя index.name='A'):
                                  #   | X   Y
df.groupby("A").sum()             # A -------
                                  # a | 4   1
                                  # b | 11  1

df.groupby("A").X.sum()           # получить Series сумм в группах для колонки X

Сначала сгруппировать по A, затем по B:

                                  #     | X  Y
df.groupby(["A","B"]).sum()       # A B |-----
                                  # a 0 | 4  1
                                  # b 0 | 5  0
                                  # 1 6 | 1
Теперь индекс - это не одно число а пара чисел:
MultiIndex([('a', '0'), ('b', '0'), ('b', '1')], names=['A', 'B'])


Агрегация групп

df = pd.DataFrame({'s': [0,0,0,1,1,2],
                   'i': [1,2,1,1,3,2],
                   't': [0,0,1,0,1,0]})
Создание таблицы в которой колонка sess уникальна, а все aid собраны в множество (второй пример - с переименованием колонки "set" → "s1" "min" → "mi"):
df.groupby('s').i.agg([set, min])                  # s  set    min
                                                   # 0  {1,2}    1
df.groupby('s').agg(s1=('i',set), mi=('i',min))    # 1  {1,3}    1
                                                   # 2  {2}      2
После группировки индекс таблицы преобретает имя index.name == s.
gr0 = df[df.t==0].groupby('s').agg(s1=('i',set))   # s   s1     s2
gr1 = df[df.t==1].groupby('s').agg(s2=('i',set))   # 0   {1,2}  {1}
                                                   # 1   {1}    {3}
pd.concat([gr0, gr1], axis=1)                      # 2   {2}    NaN

Использование справочников

Связывание таблиц подобно SQL (df_ref - таблица справочник даёт свои значения в таблицу df по ключу i):
df_ref = pd.DataFrame({'i':[0,  1, 3, 4],      #    | i   v    s
                       'v':[10,20,30,40],      #  ----------------
                       's':['a','b','c','d']}) #  0 |  0  10   a
                                               #  1 |  0  10   a
df = pd.DataFrame({"i":[0,0,1,3,2] })          #  2 |  1  20   b
                                               #  3 |  3  30   c
df.merge(df_ref, on='i', how="left")           #  4 |  2  NaN  NaN
То-же если имена колонок для коннекта различные
df.merge(df_ref, left_on='i1', right_on='i', how="left")

Работа со строковыми данными

Через атрибут str получаем доступ ко всем методам типа str применительно к Series:
s.str.lower()
s.str.lower().str.strip()

Работа с временем

Объекты времени и периода времени
ts = pd.Timestamp('2022-11-13 00:00:00', tz=None)
ts.to_pydatetime()           # datetime.datetime(2022, 11, 13, 0, 0)

rng = pd.date_range('2022-11-13', periods=3, freq='D')
rng.to_pydatetime()          # list 3-х datetime
Если дата-время в csv сохранена Unix timestamp (seconds passed since 1970-1-1) то их можно конвертировать:
df = pd.DataFrame({"date": [1659304800,      # 2022-07-31 22:00:00
                            1659304904]})    # 2022-07-31 22:01:44
df.date = pd.to_datetime(df.date, unit='s')  # D,s,ms,us,ns
t1 = pd.to_datetime(df.ts.min(), unit='s')
t2 = pd.to_datetime(df.ts.max(), unit='s')
print(t1, t2, pd.Timedelta(t2 - t1) )
df.time = pd.to_datetime(df.date, unit='s')   # D,s,ms,us,ns

df['hour']    = df.time.dt.hour               # номер часа
df['weekday'] = df.time.dt.weekday            # номер дня недели