Основы науки о данных

Простые способы создания синтетического набора данных в Python

Руководство для начинающих по созданию фиктивных табличных данных

При разработке кода иногда нам нужен фиктивный набор данных. Например, мы хотим поделиться кодом и базовыми данными, но реальный набор данных является конфиденциальным, поэтому не подходит для обмена. Один из вариантов — найти и использовать подходящий игрушечный набор данных или общедоступные наборы данных. Другой вариант — создать синтетический набор данных, достаточный для вашего варианта использования. В этом посте мы рассмотрим несколько простых способов создания синтетических наборов данных в Python.

🔧 Настройка

Начнем с загрузки необходимых библиотек:

import numpy as np
import pandas as pd
from faker import Faker
from scipy.stats import skewnorm
from datetime import datetime
from sklearn.datasets import (make_regression, make_classification, 
                              make_multilabel_classification, 
                              make_blobs)
from sklearn.model_selection import train_test_split
from sklearn.ensemble import (RandomForestClassifier,
                              RandomForestRegressor)
from sklearn.multioutput import MultiOutputClassifier
from sklearn.cluster import KMeans
from sklearn.metrics import mean_squared_error, roc_auc_score
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style='darkgrid', context='talk')

Все готово, приступаем!

📍 Scikit-Learn

Scikit-learn поставляется с множеством полезных функций для создания синтетических числовых наборов данных. В этом разделе мы познакомимся с некоторыми избранными.

📌 Регрессия

Мы можем создавать наборы данных с числовыми функциями и непрерывной целью, используя функцию make_regression. Давайте создадим набор данных с 5 функциями и непрерывной целью для 1000 записей:

n = 1000
n_features = 5
seed = 123
X, y = make_regression(n_samples=n, n_features=n_features, 
                       random_state=seed)
columns = [f"feature{i+1}" for i in range(n_features)]
df = pd.concat([pd.DataFrame(X, columns=columns), 
                pd.Series(y, name='target')], axis=1)
print(df.shape)
df.head()

Это было очень прямолинейно! Мы указали количество записей для n_samples аргумента и количество функций для n_features аргумента. Мы устанавливаем начальное значение, чтобы можно было воспроизвести синтетический набор данных. Мы можем использовать этот набор данных для построения регрессионной модели, если захотим:

X_train, X_test, y_train, y_test = train_test_split(
    X, y, random_state=seed
)
model = RandomForestRegressor(random_state=seed)
model.fit(X_train, y_train)
print(f"Train | MSE: {mean_squared_error(y_train, model.predict(X_train)):.4f}")
print(f"Test | MSE: {mean_squared_error(y_test, model.predict(X_test)):.4f}")

В этом случае модель, похоже, довольно плохо подходит к обучающим данным.

📌 Классификация

Как и выше, мы можем создать набор данных с желаемым количеством числовых функций и дискретной целью с make_classification. Теперь мы попрактикуемся в создании набора данных с 5 функциями и двоичной целью для 1000 записей:

n_classes = 2
X, y = make_classification(n_samples=n, n_features=n_features, 
                           n_classes=n_classes, random_state=seed)
columns = [f"feature{i+1}" for i in range(n_features)]
df = pd.concat([pd.DataFrame(X, columns=columns), 
                pd.Series(y, name='target')], axis=1)
print(df.shape)
df.head()

В дополнение к аргументам, которые мы использовали в разделе регрессии, мы указали количество классов в аргументе n_classes. Мы продолжим и построим модель классификации:

X_train, X_test, y_train, y_test = train_test_split(
    X, y, random_state=seed
)
model = RandomForestClassifier(random_state=seed)
model.fit(X_train, y_train)
print(f"Train | ROC-AUC: {roc_auc_score(y_train, model.predict_proba(X_train)[:,1]):.4f}")
print(f"Test | ROC-AUC: {roc_auc_score(y_test, model.predict_proba(X_test)[:,1]):.4f}")

На первый взгляд кажется, что модель работает хорошо. Мы также можем создать наборы данных для задачи классификации с несколькими метками, используя make_multilabel_classification:

X, Y = make_multilabel_classification(n_samples=n, 
                                      n_features=n_features, 
                                      n_classes=n_classes, 
                                      random_state=seed)
x_columns = [f"feature{i+1}" for i in range(n_features)]
y_columns = [f"target{i+1}" for i in range(n_classes)]
df = pd.concat([pd.DataFrame(X, columns=x_columns), 
                pd.DataFrame(Y, columns=y_columns)], axis=1)
print(df.shape)
df.head()

В этом случае у нас есть две целевые метки. Используя фиктивный набор данных, мы можем построить модель классификации с несколькими метками:

X_train, X_test, Y_train, Y_test = train_test_split(
    X, Y, random_state=seed
)
model = MultiOutputClassifier(
    RandomForestClassifier(random_state=seed)
)
model.fit(X_train, Y_train)
print(f"Train | Accuracy by class: {np.round(np.mean(Y_train==model.predict(X_train), axis=0),4)}")
print(f"Test | Accuracy by class: {np.round(np.mean(Y_test==model.predict(X_test), axis=0),4)}")

📌 Кластеризация

Еще одна полезная функция — make_blobs, которая создает данные для кластеризации:

X, y = make_blobs(n_samples=n, n_features=n_features, 
                  centers=4, random_state=seed)
columns = [f"feature{i+1}" for i in range(n_features)]
df = pd.concat([pd.DataFrame(X, columns=columns), 
                pd.Series(y, name='target')], axis=1)
print(df.shape)
df.head()

В данном случае мы выбрали 4 кластерных центра с параметром centers. Хотя кластеризация не контролируется, что означает, что у нас нет целевой переменной, мы получаем кластеры в качестве целевой переменной в синтетическом наборе данных. Давайте визуализируем сумму квадратов расстояний для разных значений k:

ks = np.arange(2, 11)
sum_squared_distances = []
for k in ks:
    model = KMeans(k, random_state=seed)
    model.fit(X)
    sum_squared_distances.append(model.inertia_)
plt.figure(figsize=(6,4))
sns.lineplot(x=ks, y=sum_squared_distances)
plt.xlabel('k')
plt.ylabel('Sum of squared distances');

Это выглядит как сумма квадратов плато расстояний после k=4. Или на нас влияет предвзятость подтверждения?

Если вам интересно узнать больше, эти 4 функции, которые мы только что изучили, имеют другие полезные параметры для дальнейшей настройки и управления тем, как вы создаете синтетические наборы данных. Вы можете узнать больше о функциях Scikit-learn для создания синтетических наборов данных здесь.

📍 NumPy и панды

Мы также можем использовать numpy и pandas, широко используемые библиотеки обработки данных, для создания фиктивных наборов данных:

np.random.seed(123)
df = pd.DataFrame()
df['id'] = np.random.choice(np.arange(10**5, 10**6), n, 
                            replace=False)
df['gender'] = np.random.choice(['female', 'male'], n, 
                                p=[0.6, 0.4])
df['age'] = np.random.randint(18, 80, size=n)
df['spend'] = skewnorm.rvs(100, loc=1000, scale=500, size=n)
df['points'] = np.random.normal(loc=50, scale=10, size=n)
start_date = pd.Timestamp("2013-01-01")
end_date = pd.Timestamp("2023-02-01")
delta = (end_date-start_date).days
df['date_joined'] = start_date + pd.to_timedelta(np.random.randint(delta, size=n), 'day')
print(df.shape)
df.head()

📌 Категориальная переменная

Мы создали категориальную переменную: gender в качестве примера. Используя аргумент p, мы указали желаемые вероятности для категорий. Мы можем проверить, отражают ли сгенерированные данные это:

pd.concat([df['gender'].value_counts(normalize=True),
           df['gender'].value_counts()], axis=1)

Отлично, примерно 60:40. Если мы не укажем аргумент p, категории будут распределены равномерно.

📌 Числовая переменная

Мы создали несколько числовых переменных: id, age, spend, points.
◼️ id: Мы обеспечили уникальность 5-значного идентификатора, указав replace=False.
◼️ ️age: использовалась функция np.random.randint() для генерации случайных целых чисел в диапазоне.
◼️ spend: использовал функцию scipy.stats.skewnorm.rvs() для создания искаженной случайной числовой переменной.
◼️ points: использовал функцию np.random.normal() для создания нормально распределенной случайной числовой переменной.

Давайте сравним распределение spend и points, как показано ниже:

fig, ax = plt.subplots(2, 1, figsize=(6, 7))
sns.histplot(data=df, x='spend', ax=ax[0])
sns.histplot(data=df, x='points', ax=ax[1])
fig.tight_layout();

Мы видим, что spend имеет перекос с длинным правым хвостом, в то время как points имеет примерно нормальное распределение.

📌 Переменная даты

Наконец, мы создали переменную даты, используя pandas. Мы определили start_date и end_date и нашли случайные даты в пределах диапазона. Мы можем проверить распределение случайно выбранных дат:

plt.figure(figsize=(6,4))
sns.histplot(data=df, x='date_joined');

А теперь давайте познакомимся с интересной библиотекой.

📍 Фейкер

Faker — это библиотека для создания поддельных наборов данных. То, как мы работаем с библиотекой, довольно простое: сначала мы инициализируем объект Faker: fake = Faker(). Затем мы можем получить доступ к целому ряду методов, предлагаемых fake.<method_name()>. Например, проверьте fake.name(). Вот пример набора данных, который мы можем создать с помощью библиотеки:

df = pd.DataFrame()
fake = Faker()
fake.seed_instance(seed)
np.random.seed(seed)
start_date = datetime(1940, 1, 1)
end_date = datetime(2005, 2, 1)
for i in range(n):
    df.loc[i, 'birthday'] = fake.date_between(start_date, end_date).strftime('%Y-%m-%d')
    df.loc[i, 'first_name'] = fake.first_name()
    df.loc[i, 'last_name'] = fake.last_name()
    df.loc[i, 'email'] = f"{df.loc[i, 'first_name'].lower()}@{fake.domain_name()}"
    df.loc[i, 'phone_number'] = fake.phone_number()
print(df.shape)
df.head()

Помимо использования pandas, мы также можем добавлять даты, используя Faker, как видно из столбца birthday. Мы также создали несколько столбцов произвольного текста. Когда вам нужны фиктивные данные, эта библиотека весьма полезна, не так ли? Если вы хотите создать фиктивный текст, вот пример синтаксиса, который мы можем использовать:

corpus = [fake.sentence() for i in range(n)]
corpus[:5]

Существуют также немного разные версии этого в зависимости от того, что нам нужно: fake.sentences(), fake.paragraph() или fake.paragraphs().

В этом посте мы рассмотрели лишь некоторые из его методов. Если вы хотите узнать больше о библиотеке, вот ее документация на GitHub.

Вуаля, это было! Надеюсь, что эти простые способы создания синтетических наборов данных будут полезны при разработке кода Python.

Хотели бы вы получить доступ к большему содержанию, подобному этому? Участники Medium получают неограниченный доступ к любым статьям на Medium. Если вы станете участником, используя мою реферальную ссылку, часть вашего членского взноса пойдет непосредственно на мою поддержку.

Спасибо, что прочитали эту статью. Если интересно, вот ссылки на некоторые другие мои посты:
◼️️ От модели ML к ML Pipeline
◼️️ Объяснение моделей Scikit-learn с помощью SHAP
◼️️ 4 простых совета по построению нескольких графиков в Python
◼️ Приведение в порядок фреймов данных pandas
Простые способы визуализации данных в Python, которые вам пригодятся
◼️ 6 простых советов, как сделать красивее и индивидуальные графики в Seaborn (Python)

Пока пока 🏃 💨