Семестр 3 · Модуль 2 · Неделя 4

Series и DataFrame. Введение в Pandas

Series и метки · DataFrame из словарей · read_csv и осмотр · loc/iloc и фильтры · запись без chained indexing

2 ч теории: 5 блоков мини-лекции, 2 live-coding, совместный разбор ошибки.

На прошлом занятии — NumPy. Не открывая конспект: a = np.array([1, 2, 3]) — что вернёт a[a > 2]? Сегодня добавим к числам имена — и получим таблицы, с которыми работают все downstream-курсы.

К концу лекции вы сможете: создавать, загружать, выбирать — и не делать chained indexing

Каждый пункт закроем кодом с реальным выводом — к концу лекции вы сможете повторить его заново, закрыв конспект.

DataFrame — та же таблица, что вы будете читать из PostgreSQL в модуле 4

Что чаще получает аналитик от заказчика — чистый файл или сырую таблицу?

Четыре ступени: список → ndarray → Series → DataFrame — каждая добавляет одну возможность

lst = [1, 2, 3] # 1. список: доступ по позиции, «+» склеивает arr = np.array([1, 2, 3]) # 2. ndarray: векторные операции, но всё ещё по позиции s = pd.Series([10, 20, 30], index=['a', 'b', 'c']) # 3. Series: та же арифметика, НО метки сохраняются df = pd.DataFrame({'name': ['Аня', 'Боря', 'Вера'], 'age': [20, 21, 19]}) # 4. DataFrame: таблица «строки × колонки» name age 0 Аня 20 1 Боря 21 2 Вера 19

Pandas добавляет к NumPy метки; DataFrame добавляет к Series второе измерение.

Что нового появляется на каждой ступени — список → ndarray → Series → DataFrame?

Метки — главное, что Pandas добавляет к NumPy

СтруктураИндексация«+»Тип элементов
Список (С1)по позицииконкатенациялюбой
ndarray (модуль 1)по позициипоэлементноодин dtype
Series (новое)по метке и позициипоэлементно, метки сохраняютсяодин dtype
DataFrame (новое)loc/iloc по строкам, колонки по именипоэлементно по значениямразный по колонкам
Что вернёт [1, 2, 3] + [4, 5, 6]? А np.array([1, 2, 3]) + np.array([4, 5, 6])?

Series = ndarray + index (метки); создаётся из списка, словаря, ndarray

import pandas as pd s = pd.Series([10, 20, 30], index=['a', 'b', 'c']) s.index, s.values, s.dtype (Index(['a', 'b', 'c'], dtype='str'), array([10, 20, 30]), dtype('int64'))
pd.Series({'a': 10, 'b': 20}) # из словаря: ключи → метки pd.Series(np.array([1, 2, 3])) # из ndarray: метки по умолчанию 0,1,2

Series — одномерный массив, у каждого элемента есть имя-метка. values — знакомый ndarray, index — метки, dtype — тип элементов.

s.values — это какой тип из модуля 1? Что показывает dtype='str' у s.index?

Доступ: по метке и по позиции

s['b'] # по метке → 20 s.iloc[1] # по позиции → 20 s.loc['b'] # по метке, осознанно → 20 20 20 20

s['b'] — по имени; s.iloc[1] — по номеру места. loc/iloc отвечают явно, без догадок.

Что общего и что разное у s['b'] и s.iloc[1]?

Векторные операции работают как в NumPy, но метки сохраняются

s = pd.Series([10, 20, 30], index=['a', 'b', 'c']) s * 2
Не запуская: что вернёт s * 2? Чем это отличается от np.array([10, 20, 30]) * 2?

Индекс участвует в операциях: x + y выравнивается по меткам

x = pd.Series([1, 2, 3], index=['a', 'b', 'c']) y = pd.Series([10, 20, 30], index=['b', 'c', 'd']) x + y a NaN b 12.0 c 23.0 d NaN dtype: float64

Pandas складывает не по позициям, а по меткам: b+b, c+c. Метки без пары дают NaN — это «выравнивание по индексу», не баг.

Почему в результате появился NaN, если обе Series длины 3?

DataFrame — «Series, собранные в таблицу»: строки (index) × колонки (columns)

pd.DataFrame({ 'name': ['Аня', 'Боря', 'Вера'], 'age': [20, 21, 19], }) name age 0 Аня 20 1 Боря 21 2 Вера 19
pd.DataFrame([{'name': 'Аня', 'age': 20}, {'name': 'Боря', 'age': 21}])

DataFrame — несколько Series, объединённых в таблицу: колонки имеют имена (columns), строки — метки (index). У колонок могут быть РАЗНЫЕ типы — главное отличие от ndarray.

Сколько строк и колонок в первой таблице? Почему у неё df.values — object-массив?

df['col'] — Series, df[['a','b']] — DataFrame; это важно для .mean()

df['Age'].head(3) 0 34.8 1 19.1 2 25.5 Name: Age, dtype: float64
df[['Age']].head(3) Age 0 34.8 1 19.1 2 25.5

Одна колонка df['Age'] — Series (одномерный, «подписанный» вывод). Список колонок df[['Age']] — DataFrame (таблица с заголовком).

Какой тип у df['Age'] и у df[['Age']]? Что вернёт .mean() каждого из них?

pd.read_csv — первый шаг любого анализа

df = pd.read_csv('titanic.csv') df.shape (40, 8)

shape — первый ответ на вопрос «что у меня в руках».

Заказчик прислал CSV из Excel с разделителем «;» и заголовками не в первой строке — какие параметры вы передадите?

Кодировка cp1251: неверная → UnicodeDecodeError или «кракозябры»

df = pd.read_csv('titanic_cp1251.csv') # файл сохранён в cp1251 UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc0 in position 26: invalid start byte
df = pd.read_csv('titanic_utf8.csv', encoding='latin-1') df.head() PassengerId Name 0 1 Ðнна Ðванов 1 2 ÐаÑÐ¸Ñ ÐеÑÑов

«Кракозябры» — не ошибка данных, а ошибка чтения. Лечение: pd.read_csv('titanic.csv', encoding='cp1251') — файл из Excel/1С часто cp1251.

Текст похож на «Ðнна» или файл падает с UnicodeDecodeError — что проверить в первую очередь?

na_values и parse_dates: «-» и «N/A» перестают быть строками

# без параметров: всё str df = pd.read_csv('tiny.csv') df.dtypes date str price str note str dtype: object
# с параметрами df = pd.read_csv('tiny.csv', parse_dates=['date'], na_values=['-', 'N/A']) df.dtypes date datetime64[us] price float64 note str dtype: object
df date price note 0 2024-05-01 1200.0 ok 1 2024-05-02 NaN NaN 2 2024-05-03 800.0 fine

na_values говорит Pandas, какие строки считать пропуском, а parse_dates — какие колонки читать как даты. Не указали na_values — колонка price осталась строкой.

Почему без na_values колонка price осталась строкой?

head()/tail()/shape: сначала «посмотри глазами»

df.shape df.head(5) # первые 5 из 40 (40, 8) PassengerId Name Pclass Sex Age Fare Embarked Survived 0 1 Анна Иванов 3 male 34.8 124.23 S 0 1 2 Мария Петров 2 male 19.1 125.22 S 0 2 3 Пётр Сидоров 3 male 25.5 234.77 S 1 3 4 Ольга Козлов 3 male NaN 145.07 S 1 4 5 Ольга Новиков 3 male 25.7 121.00 S 0
df.tail(3) PassengerId Name Pclass Sex Age Fare Embarked Survived 37 38 Мария Сорокин 3 female 17.8 248.13 C 0 38 39 Иван Сергеев 1 female 33.8 223.46 S 0 39 40 Пётр Романов 1 male 40.1 188.41 S 0

head(5) + tail(3) + shape — минимум для первого знакомства: глазами уже виден пропуск Age у строки 3.

Что в выводе head(5) вы заметили глазами — что позже подтвердит info()?

info(): типы + число ненулевых — пропуски видны сразу

df.info() <class 'pandas.DataFrame'> RangeIndex: 40 entries, 0 to 39 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 PassengerId 40 non-null int64 1 Name 40 non-null str 2 Pclass 40 non-null int64 3 Sex 40 non-null str 4 Age 38 non-null float64 5 Fare 40 non-null float64 6 Embarked 39 non-null str 7 Survived 40 non-null int64 dtypes: float64(2), int64(3), str(3) memory usage: 2.6 KB

Две колонки «неполные»: Age — 38 из 40 (2 пропуска), Embarked — 39 из 40 (1). Поэтому Age — float64, а не int64: NaN не может быть целым.

Что в этом выводе сразу выдаёт проблему с данными?

describe(): статистики, но только по числовым колонкам

df.describe() PassengerId Pclass Age Fare Survived count 40.000000 40.000000 38.000000 40.000000 40.00000 mean 20.500000 2.275000 28.147368 131.018000 0.37500 std 11.690452 0.876693 10.300505 72.675062 0.49029 min 1.000000 1.000000 9.300000 10.290000 0.00000 25% 10.750000 1.000000 18.950000 73.027500 0.00000 50% 20.500000 3.000000 27.200000 128.205000 0.00000 75% 30.250000 3.000000 34.675000 195.745000 1.00000 max 40.000000 3.000000 49.200000 248.130000 1.00000

В describe() попадают только числовые колонки (int64/float64). count у Age — 38, потому что 2 пропуска: статистики считаются по непустым значениям.

Какие колонки НЕ попадут в describe() и почему? Что значит Survived.mean() = 0.375?

value_counts(normalize=) и unique/nunique для категориальных

df['Survived'].value_counts(normalize=True) Survived 0 0.625 1 0.375 Name: proportion, dtype: float64
df['Sex'].value_counts() male 25 female 15 Name: count, dtype: int64
df['Embarked'].unique() # ['S', 'C', 'Q', NaN] df['Pclass'].nunique() # 3

value_counts() — частоты, normalize=True — доли (сумма 1). unique() — список значений, nunique() — их число; nunique() не считает NaN.

Для чего доли удобнее частот? Что общего у value_counts(normalize=True) и mean() для Survived?

Не уверен, что в таблице — выведи .head()/.shape/.info()

Вам дали неизвестный DataFrame — какие ТРИ метода вы вызовете?

loc — по меткам, iloc — по позициям

df.loc[3] # строка с меткой 3 (может не быть!)
df.iloc[3] # четвёртая строка
loc — искать квартиру по имени жильца, iloc — по номеру квартиры. Чем отличаются df.loc[3] и df.iloc[3]?

loc[0:5] включает границу (6 строк), iloc[5:15] — исключает (10 строк)

df.loc[0:5, 'Name':'Age'] # что за форма? сколько строк? df.iloc[5:15, [0, 2]] # а это?
Не запуская: сколько строк вернёт каждый вызов? Какая граница в каком методе включительна?

df.loc[условие, ['col1','col2']] — строки и колонки одним вызовом

df.loc[df['Survived'] == 1, ['Name', 'Age']] Name Age 2 Пётр Сидоров 25.5 3 Ольга Козлов NaN 5 Дмитрий Морозов 38.9 8 Иван Васильев 18.6 12 Мария Голубев 35.2 14 Мария Богданов 22.9 15 Мария Воробьёв 12.6 17 Анна Михайлов 23.6 20 Пётр Белов 49.2 21 Ольга Комаров 27.1 25 Пётр Андреев 46.3 30 Ольга Кузьмин 22.3 31 Иван Кудрявцев 18.9 35 Анна Степанов 27.3 36 Дмитрий Яковлев 12.4

Первый аргумент loc — какие строки (условие), второй — какие колонки (список имён). Один вызов — строки отфильтрованы и колонки выбраны.

Как прочитать этот вызов по-русски? «Строки, где выжили, и только колонки Name, Age».

Фильтры: df[условие] — булева маска (как в NumPy) фильтрует строки

df[df['Survived'] == 1] # все выжившие (15 строк) df[df['Age'] > 30] # старше 30 df[df['Age'].isna()] # без возраста (2 строки) PassengerId Name Pclass Sex Age Fare Embarked Survived 3 4 Ольга Козлов 3 male NaN 145.07 S 1 7 8 Мария Соловьёв 1 female NaN 132.56 C 0

df[условие] оставляет строки, где условие истинно. Условие — Series из True/False (булева маска) — та самая, что была в NumPy; внутри df[...] маска выбирает строки, а не элементы.

Что вернёт df[df['Pclass'] == 1]? Напомните маску a[a > 2] из разогрева.

«И» = & со скобками; без скобок — ValueError

# правильно: скобки вокруг каждого условия df[(df['Survived'] == 1) & (df['Age'] > 30)][['Name', 'Age']] Name Age 5 Дмитрий Морозов 38.9 12 Мария Голубев 35.2 20 Пётр Белов 49.2 25 Пётр Андреев 46.3
# типичная ошибка: & без скобок df[df['Survived'] == 1 & df['Age'] > 30] ValueError: The truth value of a Series is ambiguous.

Для Series «И» — это & (поэлементно), а не and, и каждое условие берётся в скобки. and даёт ValueError: Pandas не знает, «истина ли вся колонка».

Найди ошибку: почему падает df[df['Survived'] == 1 & df['Age'] > 30]? Как правильно?

set_index/reset_index: индекс — «адресная книга» строк

df2 = df.set_index('PassengerId') df2.loc[3] Name Пётр Сидоров Pclass 3 Sex male Age 25.5 Fare 234.77 Embarked S Survived 1 Name: 3, dtype: object
df2.reset_index().head(2) PassengerId Name Pclass Sex Age Fare Embarked Survived 0 1 Анна Иванов 3 male 34.8 124.23 S 0 1 2 Мария Петров 2 male 19.1 125.22 S 0

Индекс — «адресная книга» строк: set_index делает колонку индексом, и loc выбирает по ней; reset_index возвращает индекс в колонку. Оба возвращают НОВЫЙ DataFrame — присваивайте результат.

После set_index('PassengerId') — что вернёт df2.loc[3]?

df['Age'][df['Age'] > 30] = 99 — «запись в никуда» + Warning/Error

df['Age'][df['Age'] > 30] = 99 # «хочу заменить возраст старше 30 на 99» ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. Such chained assignment never works to update the original DataFrame or Series...
(df['Age'] == 99).sum() # записалось ли? 0

Код не упал, но и не записал: заменилось 0 строк. Это chained indexing — самый коварный риск недели. Сейчас разберём, почему так.

Код выполнился без ошибки, но (df['Age'] == 99).sum() даёт 0. Что произошло?

Почему не записалось: два вызова → промежуточная копия

# ОПАСНО: цепочка из двух вызовов df['Age'] # 1-й вызов: Series (возможно, копия) ...[...] = 99 # 2-й вызов: пишет в копию изменение «уходит в никуда» → Warning/Error
# БЕЗОПАСНО: один атомарный вызов df.loc[rows, 'col'] = value # Pandas видит и строки, и колонку # → знает, куда писать, без Warning

Правило-приём: «один атомарный вызов df.loc[rows, 'col'] = value».

Что возвращает df['Age'] — копию или вид? Именно поэтому в неё и нельзя писать.

Правильная запись: df.loc[rows, 'col'] = ... — атомарно, без Warning

# ОПАСНО: два вызова подряд df['Age'][df['Age'] > 30] = 99 ChainedAssignmentError + запись «в никуда»
# ПРАВИЛЬНО: один атомарный вызов df.loc[df['Age'] > 30, 'Age'] = 99 (df['Age'] == 99).sum() 18

Запись — только через df.loc[rows, 'col'] = value: один вызов, в котором Pandas видит и строки, и колонку. Строка с df['col'][...] в вашем коде — почти наверняка ошибка.

Как записать признак is_first_class без Warning?

Типичные ошибки недели 4

  1. Chained indexing df['a']['b'] = ... — запись «в никуда» + Warning/Error.
  2. Путаница loc/iloc: метки vs позиции, включительность границ.
  3. Числовая колонка стала object/str после чтения (примеси, NaN) — не проверили dtypes.
  4. Забытая кодировка cp1251 — UnicodeDecodeError или «кракозябры».
  5. df['col'] при записи возвращает копию — записывать через df.loc[rows, 'col'] = ....
На этом занятии мы разобрали пять ошибок, которые вы совершите на семинаре, — и теперь они вас не удивят.

Неделя 4 проекта: загрузить датасет, осмотреть, ответить на 3–4 вопроса

Проект модуля сдаётся на неделе 6 — вы уже сделали его первый этап.

Как сегодняшние слайды превращаются в ячейки ноутбука: загрузка → info() → value_counts → фильтры?
Стоп-вопросы · мини-самооценка

Четыре вопроса недели — ответьте письменно за 2 минуты

  1. Чем loc отличается от iloc и когда какая граница включительна?
  2. Как выбрать 3 колонки и отфильтровать строки одним выражением?
  3. Что выведет describe() и какие колонки в него попадут?
  4. Почему df['a']['b'] = 5 может не записать значение?
Это не контроль, а сигнал преподавателю, что повторить перед семинаром.

Семинар и ДЗ недели 4: мост к практике

Откройте семинар-4.md и загрузите titanic.csv.

Сегодня вы научились…

One-minute paper: запишите одним-двумя предложениями — главное, что вы поняли сегодня, и один вопрос, который остался.

Два правила на память: «один df.loc» и «не уверен, что в таблице — выведи .head()/.info()».