Data Science на прикладі датасету вина

Привіт, я Андрій Салата, Principal Data Scientist / Data Architect в компанії Sigma Software. Працюю в IT понад 15 років, із них понад сім років займаюся Data Science і викладаю курс із цієї дисципліни в Sigma Software University. Раніше я вже писав оглядові статті про цю дисципліну, а тепер вирішив розібрати на прикладі, чим саме займається Data Scientist.

В цій статті я пропоную розібрати конкретний датасет — так називається набір даних, з яким працюють спеціалісти з Data Science. Ми опишемо цей датасет, проаналізуємо, зробимо візуалізації та побудуємо моделі. Щоб було цікавіше, я обрав доволі веселий датасет, який складається з даних про вина відомого португальського бренду. Давайте у нього зануримося.

Знайомимось із датасетом

Отже, ми отримали якесь завдання і починаємо знайомство з даними. Про що вони? Ці дані відображають різні характеристики цих вин: кислотність, наявність різних компонентів і хімічних сполук, щільність, вміст алкоголю тощо. Усього дванадцять характеристик, одна з яких — оцінка за 10-бальною шкалою, яку те чи інше вино отримало від професійних сомельє.

Отже, маємо об’єктивні хімічні та фізичні характеристики вина і маємо оцінку якості цих вин. Ідея полягає в тому, щоб на базі цього датасету побудувати модель, яка буде прогнозувати якість вина (і відповідну оцінку від сомельє) залежно від об’єктивних характеристик вина. Погодьтеся, було б цікаво, знаючи склад і пропорції вина, більш-менш точно прогнозувати, чи це буде якісне вино, яке високо оцінять фахівці, чи якийсь шмурдяк. Спробуймо!

Досліджуємо змінні

Перше, що ми маємо зробити — провести так званий Exploratory Data Analysis (EDA), тобто спробувати знайти в нашому датасеті певні закономірності та аномалії. Також цей аналіз передбачає перевірку гіпотез і припущень за допомогою інструментів статистики та графічних зображень. Ну а потім на базі сформульованої гіпотези ми спробуємо побудувати модель.

Перш ніж робити висновки з даних, важливо дослідити всі змінні (у цьому випадку — характеристики вина). Нам доступні лише фізико-хімічні (вхідні) та сенсорні (вихідні) змінні (наприклад, немає даних про типи винограду, бренд вина, ціну продажу вина тощо).

Ось наші змінні:

  • fixed acidity;
  • volatile acidity;
  • citric acid;
  • residual sugar;
  • chlorides;
  • free sulfur dioxide;
  • total sulfur dioxide;
  • density;
  • pH;
  • sulphates;
  • alcohol.

І є output variable, наша цільова характеристика — це якість вина від нуля до десяти за десятибальною шкалою.

Output variable (based on sensory data):

  • quality (score between 0 and 10).

Зчитування даних і первинний EDA

Ознайомившись із даними, Data Scientist має поставити перед собою правильні питання. Перше, що ми хочемо зрозуміти — як виглядає цей датасет, які його характеристики розподілу і як виглядають показники в ньому. Далі — чи можемо ми знайти якісь пропуски та заповнити їх за певною логікою?

[ ]
import numpy as np # linear algebra
import pandas as pd # data processing
import seaborn as sns
import matplotlib.pyplot as plt
import os
#Setting Style for Plotting
plt.style.use('fivethirtyeight')

Як бачимо, Python-бібліотеки, які використовуються безпосередньо для роботи з даними — це, в основному, Numpy і Pandas.

Також може використовуватися Seaborn — це бібліотека, яка дозволяє робити красиві візуалізації. Вона спирається на потужну бібліотеку візуалізації Matplotlib, яка власне й дозволяє візуалізації. Seaborn — це по суті надбудова над Matplotlib, яка полегшує роботу і розширює можливості.

Отже, спочатку зчитуємо наші дані (можуть бути локально, або на Google Drive, як у нас):

[ ]
from google.colab import drive
drive.mount('/content/drive', force_remount=True)
local_dir = os.path.abspath(os.getcwd())
if local_dir != 'drive/My Drive/Colab Notebooks/data':
  os.chdir('drive/My Drive/Colab Notebooks/data')
Mounted at /content/drive

Потім завантажуємо їх:

[ ]
df = pd.read_csv('winequality-red.csv')
print('Dataset dimentions' + str(df.shape))
df.head()

І, нарешті, дивимося на наш датасет. Розглянемо, наприклад, перші п’ять значень (п’ять різних вин). Ми бачимо, що в них відрізняються показники кислотності, кількість залишкового цукру, сульфатів, щільність, pH, вміст спирту і так далі. І в кінці наш цільовий показник — оцінка якості вина. Ось так і будуть виглядати наші дані.

Далі ми можемо подивитися на shape, тобто на кількість наших даних:

[ ]
df.shape
(1599, 12)

Ми бачимо, що у нас майже 1600 рядків (очевидно, кількість протестованих вин) і 12 характеристик, за якими вони вимірюються.

Ми також можемо застосувати команду info і перевірити, чи є у нас порожні значення або пропуски:

[ ]
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1599 entries, 0 to 1598
Data columns (total 12 columns):
 #   Column                Non-Null Count  Dtype  
---  ------                --------------  -----  
 0   fixed acidity         1599 non-null   float64
 1   volatile acidity      1599 non-null   float64
 2   citric acid           1599 non-null   float64
 3   residual sugar        1599 non-null   float64
 4   chlorides             1599 non-null   float64
 5   free sulfur dioxide   1599 non-null   float64
 6   total sulfur dioxide  1599 non-null   float64
 7   density               1599 non-null   float64
 8   pH                    1599 non-null   float64
 9   sulphates             1599 non-null   float64
 10  alcohol               1599 non-null   float64
 11  quality               1599 non-null   int64  
dtypes: float64(11), int64(1)
memory usage: 150.0 KB

Бачимо, що у нас немає пропусків, нульові значення відсутні. Це чудово — значить, наш датасет вже підготовлений і немає потреби його вичищати чи заповнювати порожні місця.

Описова статистика

Тепер ми маємо порахувати якусь описову статистику. На щастя, це настільки популярна операція в Data Science і в аналізі даних, що можна просто взяти готовий метод describe, який все зробить за вас. Все, що вам потрібно — ввести df (тобто ваш Data Frame), крапка, describe — і ви одразу отримуєте описові статистики, які вам дають розуміння про закон розподілу кожного показника.

[ ]
df.describe()

Що ми можемо сказати, подивившись на результати? Показник count (кількість записів) всюди однаковий — це підтверджує, що пропуски відсутні. Якби показник відрізнявся, це вказувало б, що десь є пропуски.

Далі у нас є середнє значення fixed acidity (сталої кислотності) — трохи більш як вісім. Саме по собі середнє значення ніяк не характеризує розподіл. Натомість якщо ми бачимо середнє значення і standard deviation (наступний рядок), то ми вже можемо побудувати закон розподілу.

Далі ми маємо мінімальне і максимальне значення, розподілені від 4 до 16. Середнє, нагадаю, трохи більше за 8. Робимо висновок, що наше середнє значення зсунуте в бік мінімального. Значить, наш закон розподілу, як мінімум, є трошки спотвореним.

Крім того, у нас є інформація про медіану. І ми бачимо, що медіана досить сильно відрізняється від середнього значення. Це ще одна ознака того, що це закон розподілу не є нормальним.

Також у нас є інформація про 25-й і 75-й percentile. Усе разом дозволяє нам скласти певне уявлення про цей датасет.

Наприклад, зверніть увагу на вміст алкоголю (alcohol). Мінімальне значення в нас 8,4, максимальне — 14.9, середнє 10,4, тобто розподіл теж тяжіє до нижньої позначки. Значить більшість вин у нашому датасеті доволі легкі, з низьким вмістом спирту.

Як зрозуміти цільовий розподіл змінних і чому це так важливо

Тепер давайте викличемо будь-яку з характеристик, наприклад, нашу цільову:

[ ]
df.quality.unique()
array([5, 6, 7, 4, 8, 3])

Можемо подивитися, які взагалі у нас є значення оцінки якості. На шкалі від 1 до 10 у вин із нашого датасету є оцінки з 3 до 8. Тобто геть неякісних і супер’якісних вин у датасеті не представлено. Тому, якщо ми будуватимемо модель, і вона нам прогнозуватиме якісь значення типу одинички чи десятки, то це, скоріш за все, якась аномалія. Як бачимо, експерти таких значень не виставляють.

Також ми можемо подивитися частотний розподіл — які оцінки скільки разів зустрічаються в нашому датасеті:

[ ]
df.quality.value_counts()
5    681
6    638
7    199
4     53
8     18
3     10
Name: quality, dtype: int64

Бачимо, що переважна більшість вин із нашого датасету отримала оцінки 5 і 6. Майже 200 вин отримали високу сімку і понад 50 вин — четвірку, а вісімку і трійку отримали лише кільканадцять вин. Така картина типова для більшості законів розподілу.

Ми можемо також відобразити це на частотній діаграмі:

df['quality'].hist()

Подібним чином можемо проаналізувати і проілюструвати розподіл за вмістом алкоголю:

[ ]
sns.lineplot(df.alcohol.quantile(np.arange(0,1,0.1)))
<Axes: ylabel='alcohol'>

Що крутіший графік, то менше в нашому датасеті вин із відповідною характеристикою. Таким чином, робимо висновок, що в датасеті багато вин міцністю близько 9,5, а переважна більшість вин мають рівень алкоголю від 9,5 до 11,5.

А от якщо ми проаналізуємо вміст лимонної кислоти, то побачимо доволі рівномірний розподіл:

sns.lineplot(df["citric acid"].quantile(np.arange(0,1,0.1)))
<Axes: ylabel='citric acid'>

Отже, за кожною характеристикою вина ми можемо проаналізувати та візуалізувати форму розподілу.

Шукаємо кореляцію (взаємозалежність) між показниками

Щоб використовувати лінійну регресію для моделювання, необхідно видалити корельовані змінні, щоб покращити вашу модель. Можна знайти кореляції за допомогою функції pandas “.corr()” і візуалізувати кореляційну матрицю за допомогою теплової карти в seaborn.

Темні відтінки представляють позитивну кореляцію, а світлі відтінки представляють негативну кореляцію. Якщо встановити annot=True, ви отримаєте значення, за якими об’єкти співвідносяться між собою в клітинках сітки.

fig, ax = plt.subplots(figsize=(15,7))
sns.heatmap(df.corr(),cmap='viridis', annot=True)
<Axes: >

На перетинах ми бачитимем кореляцію між показниками. Наприклад, чим вищий рівень pH, тим нижчий рівень fixed acidity, що цілком логічно.

Ця візуалізація дозволяє нам зрозуміти, які показники найбільше впливають на якість вина — це volatile acidity (змінна кислотність), вміст алкоголю, а також сульфатів та лимонної кислоти. Знак коефіцієнта кореляції говорить про напрямок цієї залежності.

Змінна кислотність має коефіцієнт кореляції −0,39 — таким чином, чим нижча змінна кислотність, тим вища оцінка вина. Водночас рівень, алкоголю має додатний коефіцієнт, тобто вина з більшим вмістом алкоголю переважно отримують вищі оцінки.

Важливо, що ми говоримо про узагальнену лінійну залежність. Тобто цілком можливі випадки якісних вин з низьким рівнем алкоголю, проте в основній масі тенденція є такою, як описана вище. І пам’ятаємо, що коефіцієнт кореляції в жодному разі не говорить про причинно-наслідковість зв’язку, а лише про його міць і напрямок. Ми не знаємо точно, чи дійсно професійним сомельє подобаються міцніші вина, чи винороби роблять міцнішими ті вина, які подобаються сомельє.

Як ідентифікувати викиди та аномалії

Тепер розгляньмо датасет на предмет аномалій і викидів. Є дуже популярний графік, який називається Boxplot. Він теж відображає закон розподілу і дозволяє оцінити стандартні значення та аномалії, викиди. Іноді аномалії відкидають, іноді замінюють іншими значеннями — це все залежить від стратегії, яку застосовує Data Scientist.

l = df.columns.values
number_of_columns=12
number_of_rows = int(len(l)-1/number_of_columns)
plt.figure(figsize=(number_of_columns,5*number_of_rows))
for i in range(0,len(l)):
   plt.subplot(number_of_rows + 1, number_of_columns ,i+1)
   sns.set_style('whitegrid')
   sns.boxplot(df[l[i]],color='red',orient='v')
   plt.tight_layout()

На прикладі останнього стовпчика з показником Quality ми ще раз бачимо, що більшість вин із датасету потрапляють в «бокс» оцінок 5-6. «Вуса» цього «боксу» розтягуються також до оцінок 4 і 7, а от оцінки 3 і 8 — це якраз викиди.

Як знайти нерівності розподілу

Є також багато інших графіків, які дозволяють оцінити сам розподіл. Наприклад, можемо оцінити форму розподілу, його щільність.

plt.figure(figsize=(2*number_of_columns, 5*number_of_rows))
for i in range(0,len(l)):
   plt.subplot(number_of_rows + 1, number_of_columns, i+1)
   sns.distplot(df[l[i]],kde=True)

Як бачимо на графіках, форма розподілу за більшістю показників є неправильною — окрім хіба що рівня pH. Більшість розподілів мають явно виражений зсув у лівий бік, деякі також мають декілька вершин. Це вади розподілу, і досвідчені Data Scientists знають, як давати цьому раду, перетворивши це в нормальні закони розподілу. Нормальний закон розподілу дозволяє швидше і краще навчати моделі.

Далі ми можемо оцінити характеристики зміщення і гостроти розподілу. Коефіцієнт зміщення (асиметрії) показує, на скільки ваш закон розподілу відрізняється від нормального. Так само можемо оцінити Kurtosis (коефіцієнт ексцесу).

print("Skewness  \n ",df.skew())
print("\n Kurtosis  \n ", df.kurt())
Skewness  
  fixed acidity           0.982751
volatile acidity        0.671593
citric acid             0.318337
residual sugar          4.540655
chlorides               5.680347
free sulfur dioxide     1.250567
total sulfur dioxide    1.515531
density                 0.071288
pH                      0.193683
sulphates               2.428672
alcohol                 0.860829
quality                 0.217802
dtype: float64
 Kurtosis  
  fixed acidity            1.132143
volatile acidity         1.225542
citric acid             -0.788998
residual sugar          28.617595
chlorides               41.715787
free sulfur dioxide      2.023562
total sulfur dioxide     3.809824
density                  0.934079
pH                       0.806943
sulphates               11.720251
alcohol                  0.200029
quality                  0.296708
dtype: float64

Перевіряємо розподіли та кореляції усіх змінних

Також ми можемо подивитися на ці розподіли в контексті кореляції, тобто взяти показники попарно і подивитися, як вони пов’язані між собою. Якщо це «хмаринка» в центрі графіку, то чіткої залежності між двома показниками немає.

Якщо ж хмаринка на графіку розтягнута з одного кута в інший, то ми побачимо певний паттерн — дані витягнуті вздовж якоїсь уявної лінії. Це означає, що між даними є залежність, і що ця залежність може ефективно використовуватись в побудові моделей.

Спробуємо додати сюди ще й третій показник — наш цільовий, який демонструє якість вина.

sns.pairplot(df, diag_kind='kde', hue='quality')
<seaborn.axisgrid.PairGrid at 0x7f794e158e80>

Тут ми бачимо, наприклад, що показники залишкового цукру (residual sugar) та лимонної кислоти (citric acid) доволі сильно пов’язані між собою. Також варто звертати увагу на колір точок на графіку. Якщо є чітке розділення на темні та світлі точки — саме ці показники варто використовувати в фінальній моделі.

Здійснивши такий описовий аналіз, ми вже можемо відібрати певні показники, які будуть утворювати ядро нашої моделі.

Моделювання

Безпосередньо для моделювання ми можемо використовувати інструмент sklearn — це бібліотека для машинного навчання. І також використовуємо всі знання, які ми здобули на попередніх етапах.

from sklearn.model_selection import train_test_split
new_data = df.copy()
features = new_data.drop(['quality'], axis = 1)
labels = new_data['quality']

Наш датасет ми ділимо на дві частини: X-характеристики, які ми будемо використовувати для прогнозування, і Y-характеристика (наш цільовий показник якості). А потім ще й розбиваємо їх на тренувальний датасет і той, на якому ми будемо перевіряти, чи добре працює модель.

X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size = 0.4, random_state = 42)
X_test, X_val, y_test, y_val = train_test_split(X_test, y_test, test_size = 0.5, random_state = 42)
print (len(labels), len(y_train), len(y_val), len(y_test))

Тепер спробуємо застосувати модель лінійної регресії. Як бачимо, використання і побудова тієї чи іншої моделі — це буквально декілька рядочків коду (якщо використовувати її з «коробки» — тобто без тюнінгу і додаткового налаштування).

# importing module
from sklearn.linear_model import LinearRegression
# creating an object of LinearRegression class
model1 = LinearRegression()
# fitting the training data
model1.fit(X_train,y_train)
y_pred1 =  model1.predict(X_test)

Ще важливий розділ — правильний підбір метрик, за якими ми оцінюємо, добре чи погано навчається модель. У цьому випадку ми використовуємо метрику r2, яка демонструє, наскільки модель корелює з ідеальною лінією, яку ми описуємо. Також це може бути mean_square_error і root_mean_squared error. Використовуючи ці метрики, ми будуємо модель лінійної регресії.

# importing r2_score module
from sklearn.metrics import r2_score
from sklearn.metrics import mean_squared_error
# predicting the accuracy score
score1=r2_score(y_test,y_pred1)
print('R2 score is: ',score1)
print('mean_sqrd_error(MAE) is: ',mean_squared_error(y_test,y_pred1))
print('root_mean_squared error(RMSE) is: ',np.sqrt(mean_squared_error(y_test,y_pred1)))
Тепер рахуємо показники:
R2 score is:  0.338322640280049
mean_sqrd_error(MAE) is:  0.39229584165974557
root_mean_squared error(RMSE) is:  0.6263352470201127

Бачимо, що наш R2 score дорівнює 0,35, що не дуже добре. Чим ближче наш показник до одиниці, тим краще наша модель. Наш mean_sqrd_error дорівнює 0,39, що не набагато краще.

Отже, наша модель хоч і бідно, але вміє прогнозувати наш цільовий показник якості вина. Пошук причин неточності моделі і її покращення — це і є робота спеціаліста з Data Science.

Ще одна важлива річ, з якої починається аналіз того, як поводить себе модель, це feature importance, тобто важливість тих показників, які ми використовуємо.

Для деяких моделей є можливість її побудувати, і це дуже круто.

feat_importances = pd.Series(model1.coef_, index=X_train.columns)
feat_importances.nlargest(25).plot(kind='barh',figsize=(10,10))

Ми бачимо, що найсильніший показник, який використовує наша модель, це щільність — density. Далі йде кількість хлоридів, вільних кислот і так далі. Показники з від’ємними значеннями (зокрема кількість сульфатів) можна виключити з моделі.

Ось із таких етапів і складається робота Data Scientist. Якщо вас зацікавив набір даних, який ми розглядали — він доступний у репозиторії машинного навчання UCI, а також на Kaggle.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось9
До обраногоВ обраному8
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
plt.figure(figsize=(number_of_columns,5*number_of_rows))
for i in range(0,len(l)):
plt.subplot(number_of_rows + 1, number_of_columns ,i+1)
sns.set_style(’whitegrid’)
sns.boxplot(df[l[i]],color=’red’,orient=’v’)

Вибачайте, Ви код пишете в vi? Це все ж Пітон, є певні вимоги. Ріже око такий «стиль».

Дуже рекомендую поставити лінтер в ІДЕ.

number_of_rows = int(len(l)-1/number_of_columns)

Тут дуже не вистачає пробілів між знаками.

Риторичне питання, але чому в ДС зазвичай самий трешовий код на пайтоні? 😅
l, model1, модуль os

Від принципала очікуєш прям витвір мистецтва а не оце от все

В ДС більшість коду на пайтоні.
Навіщо код для EDA робити підтримуваним і т.п.? Він же треба щоб один раз вивести графіки і зрозуміти які є дані.

П.С. змінні справді можна було б називати якось нормально навіть для такого коду одноразового використання.

EDA — мабуть основний і найчастіший елемент ДС, ДА і ДЕ
Чому не зробити його гарненьким одразу?
Сьогодні csv на 12 колонок, завтра паркет на 200
Оці зсуви +1 і −1 туди ж

ПС
Взагалі радив би дивитись в сторону від пандас — не думаю що зараз в продакшн проектах даних не забагато для цього фреймворка, а мігрувати потім це все в той самий спарк — теж якісь зусилля додаткові

Яку гіпотезу ми перевіряли? Які висновки (цього блоку в статті наразі чомусь немає) ми можемо зробити за результатами роботи?

Якщо ви прочитали статтю і зрозуміли, що вам цікаво розбиратися в даних, розповідати історії про них і презентувати візуалізації — запрошую на курс із Data Science в Sigma Software University, де я буду одним із викладачів. Стартуємо вже наступного вівторка, 4 червня. Реєстрація за посиланням —
bit.ly/4c8gC1d

Підписатись на коментарі