Семестр 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
- создавать
Series и DataFrame и объяснять, чем они отличаются от списка и ndarray;
- загружать таблицу через
pd.read_csv с корректной кодировкой и параметрами;
- осматривать данные:
head/info/describe/value_counts;
- выбирать строки и колонки через
loc/iloc и фильтры, не допуская chained indexing;
- менять индекс через
set_index/reset_index.
Каждый пункт закроем кодом с реальным выводом — к концу лекции
вы сможете повторить его заново, закрыв конспект.
DataFrame — та же таблица, что вы будете читать из PostgreSQL в модуле 4
- Почти все downstream-курсы начинаются строкой
import pandas as pd:
Анализ данных, Мат. статистика, ML, NLP, Базы данных.
- Реальные данные — это таблицы: выгрузки из БД, CSV с продажами, ответы опросов,
логи. Pandas — стандарт работы с ними.
- Кадр-обещание:
df = pd.read_csv('sales.csv') →
df.info() — это и есть начало анализа (EDA).
- DataFrame — та же таблица, которую в модуле 4 вы будете читать из PostgreSQL:
что освоите здесь, пригодится и в SQL, и в отчётах на работе.
Что чаще получает аналитик от заказчика — чистый файл или
сырую таблицу?
Четыре ступени: список → 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)
sep — разделитель: для CSV по умолчанию ,; для «;»-файлов из Excel — sep=';';
header — какая строка заголовки (по умолчанию 0);
index_col — какую колонку сделать индексом;
usecols — загрузить только нужные колонки; nrows — первые N строк для быстрой проверки.
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()
- Главный страх новичка в Pandas — «не понимаю, что у меня в переменной».
- Лечится одной привычкой: не гадать, а выводить —
.head() (как выглядит),
.shape (сколько строк/колонок), .info() (типы и пропуски).
- Это и есть «отладка через вывод» — она работает весь курс, включая
merge и groupby на неделе 6.
Вам дали неизвестный DataFrame — какие ТРИ метода вы вызовете?
loc — по меткам, iloc — по позициям
df.loc[3] # строка с меткой 3 (может не быть!)
df.iloc[3] # четвёртая строка
df.loc[...] — выбор по меткам index (и именам колонок):
«я хочу строку, которая называется…».
df.iloc[...] — выбор по позициям (числам от 0):
«я хочу строку номер…».
- Срезы списков вы знаете —
iloc работает как они (по позиции);
loc — новое поведение.
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
- Chained indexing
df['a']['b'] = ... — запись «в никуда» + Warning/Error.
- Путаница
loc/iloc: метки vs позиции, включительность границ.
- Числовая колонка стала
object/str после чтения (примеси, NaN) — не проверили dtypes.
- Забытая кодировка
cp1251 — UnicodeDecodeError или «кракозябры».
df['col'] при записи возвращает копию — записывать через df.loc[rows, 'col'] = ....
На этом занятии мы разобрали пять ошибок, которые вы совершите
на семинаре, — и теперь они вас не удивят.
Неделя 4 проекта: загрузить датасет, осмотреть, ответить на 3–4 вопроса
- Выбрать датасет: Kaggle Titanic или открытый набор ≥ 1000 строк, ≥ 5 признаков.
- Загрузить через
read_csv с корректной кодировкой; вывести info/describe.
- Ответить на 3–4 вопроса markdown-блоком с числовым обоснованием. Пример:
«Какой процент пассажиров выжил?» →
df['Survived'].mean() → 0.375.
- Не менять данные — очистка на неделе 5.
Проект модуля сдаётся на неделе 6 — вы уже сделали его первый этап.
Как сегодняшние слайды превращаются в ячейки ноутбука: загрузка →
info() → value_counts → фильтры?
Стоп-вопросы · мини-самооценка
Четыре вопроса недели — ответьте письменно за 2 минуты
- Чем
loc отличается от iloc и когда какая граница включительна?
- Как выбрать 3 колонки и отфильтровать строки одним выражением?
- Что выведет
describe() и какие колонки в него попадут?
- Почему
df['a']['b'] = 5 может не записать значение?
Это не контроль, а сигнал преподавателю, что повторить перед семинаром.
Семинар и ДЗ недели 4: мост к практике
- Семинар: загрузка Titanic, первичный EDA, фильтры,
loc/iloc —
три уровня: базовый 4.1–4.2, стандартный 4.3–4.4, продвинутый 4.5 overview(df).
- ДЗ: EDA-ноутбук по своему датасету с 3–4 вопросами и числовыми
ответами (репозиторий
module2-eda, ноутбук eda_part1.ipynb).
- Далее: неделя 5 — очистка данных (пропуски, дубликаты, типы). Без
неё не работает ни один анализ.
Откройте семинар-4.md и загрузите titanic.csv.
Сегодня вы научились…
- создавать
Series/DataFrame и объяснять, что добавляют метки;
- загружать CSV с правильной кодировкой и параметрами (
na_values, parse_dates);
- осматривать данные:
head/info/describe/value_counts;
- выбирать строки и колонки через
loc/iloc и фильтры, зная границы;
- записывать значения через
df.loc[rows, col] = ... — без chained indexing.
One-minute paper: запишите одним-двумя предложениями — главное,
что вы поняли сегодня, и один вопрос, который остался.
Два правила на память: «один df.loc» и «не уверен,
что в таблице — выведи .head()/.info()».