Этот туториал практически не учит pandas, будут только рассмотрены загрузка и csv, немного индексирования и работа с категориальными атрибутами.
Если хотите (в конечном счёте придется) изучить pandas чуть подробнее:
Нормальные туториалы - http://pandas.pydata.org/pandas-docs/stable/tutorials.html
Шпаргалка - http://pandas.pydata.org/Pandas_Cheat_Sheet.pdf
Pandas за 10 (ложь) минут https://pandas.pydata.org/pandas-docs/stable/10min.html#min
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(956) # для обеспечения повторяемых результатов
Pandas предназначена для различных манипуляций с данными и их анализа. Ключевые типы в Pandas - Series (одномерный проиндексированный массив) и DataFrame - проиндексированная таблица. Индексы по умолчанию - это числа 0..N-1, но в целом они могут быть любыми, например строками и датами (для анализа временных рядов). Построить Series можно из массива, DataFrame - из матрицы, хотя есть и иные методы. Данные не обязательно должны быть числовыми. Внутри как правило все это хранится в виде numpy-массивов, особенно в гомогенном случае. Series и DataFrame можно скармливать практически в любое место, где ожидается numpy-массив.
series = pd.Series([1,5,2,9])
print(series)
print('----')
print(np.exp(series))
df = pd.DataFrame(np.random.random(size=18).reshape(6,3))
df
Во фреймах можно назначить имена столбцам и, как уже ранее говорилось, индекс строкам.
df = pd.DataFrame(np.random.random(size=18).reshape(6,3), columns=['A', 'B','C'], index=list('абвгде'))
df
Доступ к элементам или отдельным строкам и столбцам можно проводить через их номера, либо через их метки (индексы и номера столбцов).
at и iat обеспечивают доступ к отдельным элементам.
print(df.iat[2,1])
print(df.at['в', 'B'])
loc и iloc более гибки.
print(df.loc['в', 'B'])
print(df.loc['в']) #взятие строки с индексом в
df.loc['б':'д']# включая 'д'
df.loc[:,'A'] #взять один столбец
df.loc[:,['A', 'C']] #Все строки и два отобранных столбца
df.loc[list('авд'),'A':'B']
df.iloc[1:4] # 4 не включается
df.iloc[1:4,2]
Уберем индекс ибо обычно он нам не нужен
df = pd.DataFrame(np.random.random(size=18).reshape(6,3), columns=['A', 'B','C']) # Уберем индекс
df
df.iloc[1:3]
df.loc[1:3]
Несмотря на числовой индекс, срезы через loc по прежнему инклюзивные
Есть также альтернативные методы доступа
df[1:3] # аналогично iloc
Но если в квадратные скобки отправляется один элемент, то это считается столбцом
df['A']
Если имя столбца соответствует синтаксическим правилам имен переменных в Python, и не конфликтуют с другими атрибутами и методами DataFrame
df.A
plt.scatter(df.A, df.C)
plt.xlabel('A')
plt.ylabel('B')
plt.show()
Можно проводить фильтрацию аналогично numpy
df > 0.5
df[df > 0.5]
df[df.A > 0.5]
NaN - типичное значения для отсутствующих данных notnull() находит не NaN значения
df[df > 0.5].notnull()
df
Поразрядные операции можно использовать в качестве логических связок
df[(df.A > 0.5) & (df.B < 0.4)]
df[(df.A > 0.5) | (df.B > 0.25)]
df[~(df.A > 0.5)]
Фреймы можно объединять
df
df2 = pd.DataFrame(np.random.randint(1,10,size=6).reshape(2,3))
pd.concat([df,df2])
df3 = pd.DataFrame(np.random.randint(1,10,size=6).reshape(2,3), columns=['A','B','C'])
pd.concat([df, df3])
pd.concat([df, df3], axis=0, ignore_index=True)
df4 = pd.DataFrame(np.random.randint(1,10,size=12).reshape(6,2), columns=['D', 'E'])
df4
pd.concat([df, df4], axis=1) # Конкатенация по столбцам
df
Можно также добавлять новые столбцы на месте (не копируя фрейм)
df['Total'] = np.sum(df, axis=1) # Сумма строк
df
Иммутабельное удаление столбцов
df.drop(['A','B'],axis=1)
Мутабельное удаление
df.drop('Total', axis=1, inplace=True)
df
Чтение csv-файлов. Практически все наборы данных в лабораторной представлены в виде csv-файлов с заголовками. pandas позволяет легко их читать в dataframы, при этом заголовок становится именами столбцов.
with open('datasets/Davis.csv') as f:
for i, line in enumerate(f):
print(line)
if i > 5:
break
davis = pd.read_csv('datasets/Davis.csv') # у метода много параметров, но многие наборы данных подогнаны под pandas
davis.sample(10) # 10 случайных строк
Здесь затесался лишний атрибут без имени
davis = davis.drop("Unnamed: 0", axis=1)
davis.head(4) # первые 4 строки
Посмотрим, есть ли здесь NaN значения
davis.isna().any()
Мы можем отфильтровать весь набор данных (не делайте так если модель не использует атрибуты с отсутствующими значениями). Есть также другие стратегии работы с ними, например, вставка медианы или среднего значения на место отсутствующего.
davis_filtered = davis.dropna(axis=0, how='any') # удалить все строки с NaN значениями
davis_filtered.isna().any()
print(davis_filtered.shape)
print(davis.shape)
Составим зависимость заявленного роста от настоящего
plt.scatter(davis_filtered.height, davis_filtered.repht)
plt.xlabel('height')
plt.ylabel('repht')
xmin, xmax = davis_filtered.height.min(), davis_filtered.height.max()
plt.plot([xmin,xmax], [xmin,xmax], color='red', linestyle='--')
plt.show()
Можно заметить, что здесь есть выброс.
davis_filtered.loc[:,['height', 'repht']][davis_filtered.height < 80]
Вероятная причина - дюймовая система
57 * 2.54
Не похоже... 5 футов, семь дюймов?
5 * 2.54 * 12 + 2.54 * 7
Тоже, не очень... Выкинем, чтобы не портило картину
davis_filtered = davis_filtered[davis_filtered.height > 120]
plt.scatter(davis_filtered.height, davis_filtered.repht)
plt.xlabel('height')
plt.ylabel('repht')
xmin, xmax = davis_filtered.height.min(), davis_filtered.height.max()
plt.plot([xmin,xmax], [xmin,xmax], color='red', linestyle='--')
plt.show()
Можно заметить, что в большинстве случае люди указывают больший рост, чем замеряется
np.median(davis_filtered.repht - davis_filtered.height)
Один из вариантов фильтрации выбросов - по квантилям. Квантиль для вероятности p - это значение, которое случайная величина не будет превышать с вероятностью p. Например, с вероятность 0.05 вес меньше 50, и с вероятностью 0.95 вес меньше 90. Таким образом мы можем вытащить 90% посередине (в нашем случае - по росту)
davis_filtered.quantile([.05,.95])
davis_filtered.height.quantile([.05,.95])
a, b = davis_filtered.height.quantile([.05,.95])
filter_height = davis_filtered[(davis_filtered.height > a) & (davis_filtered.height < b)]
print(filter_height.shape)
print(davis.columns) # коллекция из имен столбцов
print(davis.index) # коллекция из индексов
print(davis.values[::10]) # преобразовать в numpy массив и вывести каждую 10 строку
Категориальные атрибуты имеют ограниченную область значений, состоящую из качественных категорий. Во многих случаях, между категориями нельзя задать отношение порядка (пол, раса), но если это возможно (оценка, отзыв плохо-средне-хорошо) то такая переменная называется ординальной.
Категориальные переменные требуют кодировки, чтобы использоваться в обучаемых моделях, работающих с числами. Сгенерируем набор данных из трех категорий для примера.
mid_income = np.random.normal(loc=1000, scale=300, size=10)
high_income = np.random.normal(loc=1400, scale=500, size=10)
phd_income = np.random.normal(loc=1450, scale=600, size=10)
education = ['mid_school'] * 10 + ['university'] * 10 + ['PhD'] * 10
edu_frame = pd.DataFrame(data={'education': education,
'income': np.concatenate((mid_income, high_income, phd_income))})
edu_frame
Данная переменная не может с уверенностью считаться ординальной.
from sklearn.preprocessing import OneHotEncoder, LabelEncoder
Один вариант - закодировать переменную числами от 0 до N-1, где N - число категорий
label_encoder = LabelEncoder()
labels = label_encoder.fit_transform(edu_frame.education)
labels
В принципе, нам должно быть наплевать, какое число из $[0,N)$ назначается какой категории. В данном случае категориям назначаются числа в лексикографическом порядке их названий. Но для иллюстрации, назначим их руками.
label_encoder = LabelEncoder()
label_encoder.classes_ = ['mid_school', 'university', 'PhD']
labels = label_encoder.transform(edu_frame.education)
labels
edu_frame['edu_label'] = labels
edu_frame.sample(8)
Однако если мы отправим edu_label в модель, например в линейную, то с точки зрения модели mid_school < university < PhD, что может быть разумным в некоторых контекстах, и неразумным в других. Не говоря о том, что даже если порядок присутствует, PhD не обязательно на столько больше university, как 2 > 1.
Другой вариант - ввести несколько бинарных признаков, по одному на категорию.
Такой подход называется one-hot кодировкой (одноместной), потому что для любого экземпляра данных только один из атрибутов будет равен единице.
oh = OneHotEncoder(sparse=False)
oh_enc = oh.fit_transform(labels.reshape(-1,1))
print(oh_enc[::5]) # выводим каждую 5 строку
Вместо LabelEncoder и One Hot Encoder можно также использовать метод get_dummies из pandas (хотя дальше мы продолжим использовать oh_enc). Преимущество - автоматическое назначение номеров категориям, автоматическое именование столбцов получаемого фрейма.
D = pd.get_dummies(edu_frame.education)
D.sample(5)
Теперь мы можем натренировать модель вида $income = a * mid\_school + b * university + c * PhD + d$
from sklearn.linear_model import LinearRegression
X = pd.DataFrame(data=oh_enc,columns=label_encoder.classes_, dtype=np.int32)
X.iloc[::5]
linreg = LinearRegression()
linreg.fit(X, edu_frame.income)
print(linreg.coef_)
print(X.columns)
linreg.intercept_
linreg.score(X,edu_frame.income)
Что не так с этой моделью
w = linreg.coef_
b = linreg.intercept_
xx = np.diag([1,1,1])
print(xx)
print('w =',w,'b =',b)
print(np.dot(xx, w) + b) #Вычисляем линейную функцию на всех возможных входах
b -= 500 #Убавляем из b 500
print('w =',w + 500,'b =',b) #Прибавляем к каждому весу 500
print(np.dot(xx, w + 500) + b) #и получаем те же результаты
поскольку в формуле $income = a * mid\_school + b * university + c * PhD + d*1$ атрибуты суммируются в единицу, т.е. $mid\_school + university + PhD = 1$, по сути ищется оптимальная линейная функция вида:
\begin{equation*} income = a * mid\_school + b * university + c * PhD + d*(mid\_school + university + PhD) \end{equation*}
Или \begin{equation*} income = (a + d) * mid\_school + (b + d) * university + (c + d) * PhD \end{equation*}
Очевидно, что их бесконечное множество. В данном случае одно из решений было найдено, но во многих других библотеках происходят ошибки из-за вырожденности матрицы $X$ (если добавить в нее столбец из единиц).
Чтобы избежать этой проблемы и получить интерпретируемое решение, нужно выделить $n-1$ переменных из $n$ категорий, т.е. выбросить одну из новых переменных.
linreg2 = LinearRegression()
Xreduced = X.iloc[:,1:] # все столбцы кроме первого (mid_school)
linreg2.fit(Xreduced, edu_frame.income)
print(linreg2.coef_)
print(Xreduced.columns)
linreg2.intercept_
linreg2.score(Xreduced,edu_frame.income)
Теперь модель имеет вид $income = a * university + b * PhD + d*1$. У нее следующая интерпретация:
Если education=mid_school, то $income=d$
Если education=university, то $income = a + d$
Если education=PhD, то $income = b + d$
В этом смысле, $mid\_school$ - это случай по умолчанию и модель показывает прирост/падение в зависимости от другого уровня образования. Подход расширяется на множество категориальных переменных, а также на смешанные атрибуты.
Кодировка из $n-1$ категорий называется dummy-кодировкой (но .get_dummies мы использовали чтобы получить one-hot). В случае двух категорий одной назначается 1, другой 0.
Получить dummy кодировку без страданий также можно через pandas через опцию drop_first функции pd.get_dummies
pd.get_dummies(edu_frame.education, drop_first=1).iloc[::5]
Попробуем интерпретировать переменную education как ординальную. Две основные стратегии - игнорировать порядок, как мы это сделали выше или попробовать назначить им числовые метки
lenc2 = LabelEncoder()
lenc2.classes_ = ['mid_school', 'university', 'PhD']
lenc2.classes_
edu_labels = lenc2.transform(edu_frame.education)
print(edu_labels)
Производительность линейной модели заметно падает, поскольку мы назначаем переменным числа без какого-либо основания.
linreg3 = LinearRegression()
linreg3.fit(edu_labels.reshape(-1,1),edu_frame.income)
linreg3.score(edu_labels.reshape(-1,1),edu_frame.income)
Один редкий способ дать модели информацию о порядке - rank-hot кодировка. (http://scottclowe.com/2016-03-05-rank-hot-encoder/)
В таком случае:
mid_school = [1,0,0]
university = [1,1,0]
phd = [1,1,1]
Интепретировать эти признаки можно через словосочетание "как минимум". Человек, окончивший университет, как минимум окончил школу.
Построим rank-hot из имеющейся one-hot кодировки
rX = oh_enc.copy()
print(rX[::6])
Поставим в первом (от нуля) столбце единицы там, где они есть во втором
rX[:,1][rX[:,2] == 1] = 1
Вместо наполнения нулевого столбца, просто его выбросим, ибо в rank-hot кодировке он везде равен единице
rX = rX[:,1:]
print(rX[::5])
linreg4 = LinearRegression()
linreg4.fit(rX,edu_frame.income)
linreg4.score(rX,edu_frame.income)
мы получили производительность, идентичную dummy-кодировке
print(linreg4.coef_)
print(lenc2.classes_[1:])
linreg4.intercept_
Интерпретация:
$income = income_0 + a * university + b * phd$
$income_0$ - доход при среднем образовании
$a$ - прибавка за университет при имеющемся среднем
$b$ - прибавка за кандидата при имеющемся среднем и высшем.
Pandas позволяет удобно работать с табличными данными, анализировать и фильтровать их.
Многие наборы данных на таких сайтах, как Kaggle, представлены в нативном для Pandas csv-формате.
Категориальные переменные требуют дополнительной обработки в отличие от количественных. Распространенные форматы - One Hot и Dummy кодировки. Ординальные переменные могут обрабатываться как категориальные, но при этом модель не получает информацию о порядке