Массивы NumPy и Pandas. Техники дескриптивного анализа

В мире анализа данных сложно переоценить важность эффективных инструментов для работы с большими массивами информации. В этой статье я хочу поделиться своим опытом использования двух фундаментальных библиотек Python для анализа данных: NumPy и Pandas, а также рассказать о техниках дескриптивного анализа, которые позволяют извлечь максимум полезной информации из имеющихся данных.

Фишки NumPy: высокопроизводительные вычисления с массивами

NumPy (Numerical Python) – это фундаментальная библиотека для научных вычислений в Python, без которой сложно представить современный анализ данных.

Ее центральным элементом является объект ndarray – n-мерный массив, обеспечивающий высокопроизводительные операции с большими объемами данных. Работая с различными проектами, я постоянно убеждаюсь в том, насколько эффективнее становится код при использовании векторизованных операций NumPy вместо стандартных циклов Python.

Структура и особенности ndarray

Массивы NumPy существенно отличаются от стандартных списков Python, и понимание этих различий критически важно для эффективной работы с данными. В отличие от списков, которые могут содержать элементы разных типов и хранятся разрозненно в памяти, ndarray – это гомогенная структура с фиксированным размером, где все элементы имеют одинаковый тип данных и располагаются последовательно в памяти.

Такая организация обеспечивает несколько ключевых преимуществ:

  1. Эффективное использование памяти – отсутствие избыточных метаданных для каждого элемента;
  2. Высокая скорость доступа к элементам благодаря их последовательному расположению;
  3. Возможность применения оптимизированных низкоуровневых функций для массовых операций;
  4. Легкая интеграция с кодом на C/C++ через механизм буферов памяти.

Приведу пример создания и базовых операций с массивами NumPy:

import numpy as np

# Создание массивов разными способами
arr1 = np.array([1, 2, 3, 4, 5])  # Из списка
arr2 = np.zeros((3, 4))           # Заполненный нулями массив размера 3x4
arr3 = np.ones((2, 3, 4))         # Трехмерный массив, заполненный единицами
arr4 = np.arange(0, 10, 0.5)      # Массив с равномерно распределенными значениями
arr5 = np.linspace(0, 1, 5)       # 5 равномерно распределенных точек от 0 до 1
arr6 = np.random.random((2, 2))   # Случайные числа в диапазоне [0, 1)

# Базовые атрибуты массива
print(f"Форма массива: {arr3.shape}")
print(f"Размерность массива: {arr3.ndim}")
print(f"Тип данных: {arr3.dtype}")         
print(f"Размер элемента в байтах: {arr3.itemsize}") 
print(f"Общее количество элементов: {arr3.size}")

# Переформатирование массива
reshaped = arr4.reshape(5, 4)  # Изменение формы без изменения данных
flattened = arr3.flatten()     # Преобразование в одномерный массив

Форма массива: (2, 3, 4)
Размерность массива: 3
Тип данных: float64
Размер элемента в байтах: 8
Общее количество элементов: 24

Важно отметить, что операции изменения формы, такие как reshape, не создают копию данных, если это возможно, а оперируют с оригинальным массивом через альтернативное представление. Это так называемые «view» операции, которые экономят память и ускоряют работу. Однако некоторые операции, например, flatten(), всегда создают копию данных.

Векторизованные операции в NumPy

Одно из главных преимуществ NumPy – возможность выполнять операции над всем массивом без явных циклов, что называется векторизацией. В своей практике я регулярно сталкиваюсь с задачами, где векторизованные операции ускоряют вычисления в десятки, а иногда и сотни раз по сравнению с эквивалентным кодом на чистом Python.

Рассмотрим простой пример – вычисление евклидова расстояния между двумя наборами точек:

import numpy as np
import time

# Генерируем два набора из 1 миллиона точек в 3D пространстве
points1 = np.random.random((1000000, 3))
points2 = np.random.random((1000000, 3))

# Количество точек для теста
N = 1000000

# Метод с использованием циклов Python
def euclidean_distance_loop(p1, p2):
    result = np.empty(len(p1))
    for i in range(len(p1)):
        squared_diff_sum = 0
        for j in range(len(p1[i])):
            squared_diff_sum += (p1[i][j] - p2[i][j]) ** 2
        result[i] = np.sqrt(squared_diff_sum)
    return result

# Векторизованный метод NumPy
def euclidean_distance_vectorized(p1, p2):
    return np.sqrt(np.sum((p1 - p2) ** 2, axis=1))

# Берём одинаковое подмножество для честного сравнения
p1_sub = points1[:N]
p2_sub = points2[:N]

# Сравниваем производительность
start = time.time()
dist_loop = euclidean_distance_loop(p1_sub, p2_sub)
print(f"Python loop time: {time.time() - start:.4f} seconds")

start = time.time()
dist_vectorized = euclidean_distance_vectorized(p1_sub, p2_sub)
print(f"NumPy vectorized time: {time.time() - start:.4f} seconds")

# Проверяем точность результатов
print(f"Results match: {np.allclose(dist_loop, dist_vectorized)}")

Python loop time: 4.0288 seconds
NumPy vectorized time: 0.0314 seconds
Results match: True

Результаты такого сравнения обычно показывают разницу в производительности в сотни раз в пользу векторизованного подхода. Причины такого преимущества кроются в следующем:

  1. Операции выполняются на уровне оптимизированного C-кода, а не интерпретатора Python;
  2. Используются SIMD-инструкции процессора (Single Instruction Multiple Data);
  3. Минимизируются накладные расходы на вызовы функций и проверки типов;
  4. Эффективнее используется кэш процессора благодаря последовательному доступу к памяти.

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

Продвинутые техники индексации

Гибкие возможности индексации в NumPy позволяют элегантно решать многие задачи обработки данных без громоздких циклов. В отличие от стандартных списков Python, NumPy поддерживает многомерное индексирование, индексирование булевыми массивами и расширенные срезы.

Приведу несколько примеров продвинутой индексации, которые я часто использую в своей работе:

import numpy as np

# Создаем тестовый массив
arr = np.arange(16).reshape(4, 4)
print("Исходный массив:")
print(arr)

# 1. Многомерное индексирование
row_indices = np.array([0, 2, 3])
col_indices = np.array([1, 2, 0])
# Выбираем элементы (0,1), (2,2), (3,0)
selected = arr[row_indices, col_indices]
print("\nВыбранные элементы по парам индексов:")
print(selected)  # [1, 10, 12]

# 2. Булево индексирование
mask = arr > 7
print("\nМаска для элементов > 7:")
print(mask)
print("\nЭлементы, удовлетворяющие условию:")
print(arr[mask])

# 3. Комбинированное индексирование
print("\nСтроки 1-3, значения между 5 и 12:")
complex_selection = arr[1:4, :][arr[1:4, :] > 5][arr[1:4, :][arr[1:4, :] > 5] < 12]
print(complex_selection)

# 4. Изменение определенных элементов
arr_modified = arr.copy()
# Заменим все четные числа на -1
arr_modified[arr_modified % 2 == 0] = -1
print("\nМассив с замененными четными числами:")
print(arr_modified)

# 5. Fancy indexing для переупорядочивания
shuffled_indices = np.random.permutation(4)
print("\nПеремешанные индексы:")
print(shuffled_indices)
print("\nПеремешанные строки:")
print(arr[shuffled_indices])

Исходный массив:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]]

Выбранные элементы по парам индексов:
[ 1 10 12]

Маска для элементов > 7:
[[False False False False]
[False False False False]
[ True True True True]
[ True True True True]]

Элементы, удовлетворяющие условию:
[ 8 9 10 11 12 13 14 15]

Строки 1-3, значения между 5 и 12:
[ 6 7 8 9 10 11]

Массив с замененными четными числами:
[[-1 1 -1 3]
[-1 5 -1 7]
[-1 9 -1 11]
[-1 13 -1 15]]

Перемешанные индексы:
[1 0 3 2]

Перемешанные строки:
[[ 4 5 6 7]
[ 0 1 2 3]
[12 13 14 15]
[ 8 9 10 11]]

Особое внимание стоит обратить на разницу между базовым срезом (arr[1:3]) и расширенным индексированием (arr[[1,2]]). Базовый срез создает представление исходного массива (view), изменения в котором отражаются на исходном массиве. Расширенное индексирование, напротив, всегда создает копию данных.

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

Универсальные функции (ufuncs)

Универсальные функции в NumPy представляют собой операции, которые выполняются поэлементно на массивах. Они реализованы на C и предоставляют высокопроизводительные альтернативы стандартным функциям Python. В контексте дескриптивного анализа ufuncs позволяют быстро вычислять различные статистики и трансформации данных.

Вот несколько примеров использования универсальных функций для анализа данных:

import numpy as np

# Создаем тестовый массив данных, имитирующий некоторые измерения
data = np.random.normal(100, 15, 10000)  # 10000 значений с нормальным распределением

# Базовая статистика с использованием ufuncs
print(f"Минимум: {np.min(data):.2f}")
print(f"Максимум: {np.max(data):.2f}")
print(f"Сумма: {np.sum(data):.2f}")
print(f"Среднее: {np.mean(data):.2f}")
print(f"Медиана: {np.median(data):.2f}")
print(f"Стандартное отклонение: {np.std(data):.2f}")
print(f"Дисперсия: {np.var(data):.2f}")

# Квантили и процентили
percentiles = np.percentile(data, [25, 50, 75, 90, 95, 99])
print("\nПроцентили (25%, 50%, 75%, 90%, 95%, 99%):")
for p, val in zip([25, 50, 75, 90, 95, 99], percentiles):
    print(f"{p}%: {val:.2f}")

# Подсчет элементов, удовлетворяющих условию
above_120 = np.sum(data > 120)
print(f"\nКоличество значений > 120: {above_120} ({above_120/len(data)*100:.2f}%)")

# Нормализация данных (Z-score)
z_scores = (data - np.mean(data)) / np.std(data)
print(f"\nZ-scores - среднее: {np.mean(z_scores):.2f}, станд. отклонение: {np.std(z_scores):.2f}")

# Определение выбросов (значения, отстоящие более чем на 3 стандартных отклонения)
outliers = data[np.abs(z_scores) > 3]
print(f"Количество выбросов: {len(outliers)} ({len(outliers)/len(data)*100:.2f}%)")
print(f"Значения выбросов: {outliers[:5]}...")  # Показываем первые 5 выбросов

# Логарифмическое преобразование (полезно для данных с правосторонней асимметрией)
log_data = np.log(np.abs(data))
print(f"\nПосле логарифмирования - среднее: {np.mean(log_data):.2f}, станд. отклонение: {np.std(log_data):.2f}")

# Бинаризация данных (пороговая функция)
binary_data = np.where(data > np.mean(data), 1, 0)
print(f"\nПосле бинаризации - количество 1: {np.sum(binary_data)} ({np.mean(binary_data)*100:.2f}%)")

Минимум: 46.21
Максимум: 160.61
Сумма: 999589.62
Среднее: 99.96
Медиана: 100.01
Стандартное отклонение: 14.96
Дисперсия: 223.95

Читайте также:  Расчет доверительных интервалов и уровня значимости с Python

Процентили (25%, 50%, 75%, 90%, 95%, 99%):
25%: 89.80
50%: 100.01
75%: 110.11
90%: 119.00
95%: 124.37
99%: 135.62

Количество значений > 120: 884 (8.84%)

Z-scores — среднее: -0.00, станд. отклонение: 1.00
Количество выбросов: 31 (0.31%)
Значения выбросов: [ 54.40837059 50.32427838 51.65957967 153.39431996 146.70820359]…

После логарифмирования — среднее: 4.59, станд. отклонение: 0.15

После бинаризации — количество 1: 5012 (50.12%)

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

Например, вместо последовательности:

temp1 = np.abs(data)
temp2 = np.log(temp1)
result = np.sqrt(temp2)

можно написать:

result = np.sqrt(np.log(np.abs(data)))

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

Также стоит упомянуть о возможности создания собственных универсальных функций с помощью декоратора @np.vectorize или функции np.frompyfunc(). Однако важно понимать, что такие функции не будут столь же оптимизированы, как встроенные ufuncs, и могут работать медленнее, чем хорошо векторизованный код с использованием стандартных функций NumPy.

Pandas: анализ структурированных данных

Если NumPy представляет собой фундамент для численных вычислений, то Pandas – это мощная надстройка, специализированная на работе со структурированными данными.

В своей практике я обнаружил, что Pandas особенно удобен для анализа табличных данных, временных рядов и работы с разнородной информацией. Библиотека предоставляет интуитивно понятный интерфейс для манипуляций с данными и включает множество встроенных функций для дескриптивного анализа.

Манипуляции с Dataframes

Основным объектом Pandas является Dataframe – двумерная таблица с метками строк и столбцов. По сути, Dataframe можно представить как словарь Series (одномерных массивов), где каждый Series соответствует отдельному столбцу таблицы. Такая структура идеально подходит для представления гетерогенных данных, с которыми мы часто сталкиваемся при анализе временных рядов и структурированных данных.

Рассмотрим основные способы создания и манипуляции с DataFrame:

import pandas as pd
import numpy as np

# Создание DataFrame из словаря
data = {
    'имя': ['Анна', 'Борис', 'Вера', 'Григорий', 'Дарья'],
    'возраст': [28, 34, 29, 42, 31],
    'отдел': ['Маркетинг', 'IT', 'Финансы', 'IT', 'Маркетинг'],
    'стаж': [2.5, 7, 4.2, 15, 3.7],
    'премия': [True, False, True, True, False]
}

df = pd.DataFrame(data)
print("Исходный DataFrame:")
print(df)

# Базовая информация о DataFrame
print("\nРазмерность DataFrame:", df.shape)
print("\nТипы данных в столбцах:")
print(df.dtypes)
print("\nСтатистическая информация:")
print(df.describe())

# Доступ к данным
print("\nПервый столбец (имя):")
print(df['имя'])
print("\nПервая строка:")
print(df.iloc[0])
print("\nСотрудники отдела IT:")
print(df[df['отдел'] == 'IT'])

# Добавление нового столбца
df['зарплата'] = [65000, 120000, 90000, 150000, 70000]
print("\nDataFrame с новым столбцом:")
print(df)

# Вычисляемый столбец
df['налог'] = df['зарплата'] * 0.13
print("\nDataFrame с вычисляемым столбцом:")
print(df)

# Группировка и агрегация
print("\nСредняя зарплата по отделам:")
print(df.groupby('отдел')['зарплата'].mean())

# Сортировка
print("\nDataFrame, отсортированный по зарплате (по убыванию):")
print(df.sort_values('зарплата', ascending=False))
Исходный DataFrame:
        имя  возраст      отдел  стаж  премия
0      Анна       28  Маркетинг   2.5    True
1     Борис       34         IT   7.0   False
2      Вера       29    Финансы   4.2    True
3  Григорий       42         IT  15.0    True
4     Дарья       31  Маркетинг   3.7   False

Размерность DataFrame: (5, 5)

Типы данных в столбцах:
имя         object
возраст      int64
отдел       object
стаж       float64
премия        bool
dtype: object

Статистическая информация:
         возраст       стаж
count   5.000000   5.000000
mean   32.800000   6.480000
std     5.630275   5.040536
min    28.000000   2.500000
25%    29.000000   3.700000
50%    31.000000   4.200000
75%    34.000000   7.000000
max    42.000000  15.000000

Первый столбец (имя):
0        Анна
1       Борис
2        Вера
3    Григорий
4       Дарья
Name: имя, dtype: object

Первая строка:
имя             Анна
возраст           28
отдел      Маркетинг
стаж             2.5
премия          True
Name: 0, dtype: object

Сотрудники отдела IT:
        имя  возраст отдел  стаж  премия
1     Борис       34    IT   7.0   False
3  Григорий       42    IT  15.0    True

DataFrame с новым столбцом:
        имя  возраст      отдел  стаж  премия  зарплата
0      Анна       28  Маркетинг   2.5    True     65000
1     Борис       34         IT   7.0   False    120000
2      Вера       29    Финансы   4.2    True     90000
3  Григорий       42         IT  15.0    True    150000
4     Дарья       31  Маркетинг   3.7   False     70000

DataFrame с вычисляемым столбцом:
        имя  возраст      отдел  стаж  премия  зарплата    налог
0      Анна       28  Маркетинг   2.5    True     65000   8450.0
1     Борис       34         IT   7.0   False    120000  15600.0
2      Вера       29    Финансы   4.2    True     90000  11700.0
3  Григорий       42         IT  15.0    True    150000  19500.0
4     Дарья       31  Маркетинг   3.7   False     70000   9100.0

Средняя зарплата по отделам:
отдел
IT           135000.0
Маркетинг     67500.0
Финансы       90000.0
Name: зарплата, dtype: float64

DataFrame, отсортированный по зарплате (по убыванию):
        имя  возраст      отдел  стаж  премия  зарплата    налог
3  Григорий       42         IT  15.0    True    150000  19500.0
1     Борис       34         IT   7.0   False    120000  15600.0
2      Вера       29    Финансы   4.2    True     90000  11700.0
4     Дарья       31  Маркетинг   3.7   False     70000   9100.0
0      Анна       28  Маркетинг   2.5    True     65000   8450.0

Важно отметить несколько особенностей Pandas-овских датафреймов, которые делают его особенно удобным для анализа данных:

  1. Автоматическое выравнивание индексов – при операциях с несколькими DataFrame данные автоматически выравниваются по индексам, что минимизирует ошибки при объединении неполных данных;
  2. Встроенная обработка пропущенных значений – Pandas предлагает множество методов для обнаружения, заполнения или удаления NA/NaN значений;
  3. Интеллектуальное преобразование типов – при загрузке данных Pandas пытается автоматически определить оптимальные типы данных для столбцов;
  4. Встроенная поддержка иерархических индексов – позволяет работать с многомерными данными в плоском представлении.

Благодаря этим возможностям, датафрейм Pandas стал de-facto стандартом для представления табличных данных в экосистеме Python, и я рекомендую его использование для большинства задач дескриптивного анализа.

Оптимизация загрузки и предобработки данных

Прежде чем приступить к дескриптивному анализу, необходимо загрузить и правильно подготовить данные. Pandas предоставляет множество функций для чтения данных из различных источников, включая CSV, Excel, SQL, JSON и многие другие форматы.

В следующем примере я покажу полный процесс загрузки, очистки и подготовки данных к анализу:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime

# Предположим, у нас есть CSV файл с данными о продажах
# Создадим сначала такой файл для демонстрации
sales_data = pd.DataFrame({
    'дата': pd.date_range(start='2023-01-01', periods=100),
    'товар': np.random.choice(['Телефон', 'Ноутбук', 'Планшет', 'Наушники'], 100),
    'регион': np.random.choice(['Москва', 'Санкт-Петербург', 'Новосибирск', 'Екатеринбург'], 100),
    'продавец': np.random.choice(['Иванова', 'Петров', 'Сидоров', 'Кузнецова'], 100),
    'количество': np.random.randint(1, 10, 100),
    'цена': [round(np.random.uniform(5000, 150000), -3) for _ in range(100)]
})

# Внесем немного "шума" в данные - пропущенные значения, дубликаты, ошибки
sales_data.loc[np.random.choice(100, 5), 'количество'] = np.nan  # Пропущенные значения
sales_data.loc[np.random.choice(100, 3), 'продавец'] = None  # Еще пропущенные значения
sales_data.loc[np.random.choice(100, 2), 'цена'] = -1000  # Ошибочные значения
# Добавим несколько дубликатов
duplicates = sales_data.sample(5).copy()
sales_data = pd.concat([sales_data, duplicates], ignore_index=True)

# Сохраним в CSV для демонстрации
csv_path = 'sales_data.csv'
sales_data.to_csv(csv_path, index=False)

# Теперь загрузим данные и проведем предварительную обработку
df = pd.read_csv(csv_path)

# 1. Исследуем базовую структуру данных
print(f"Размер DataFrame: {df.shape}")
print("\nПервые 5 строк:")
print(df.head())
print("\nИнформация о DataFrame:")
print(df.info())
print("\nСтатистика по числовым столбцам:")
print(df.describe())

# 2. Проверяем и обрабатываем пропущенные значения
print("\nКоличество пропущенных значений по столбцам:")
print(df.isnull().sum())

# Заполняем пропущенные значения
df['количество'].fillna(df['количество'].median(), inplace=True)
df['продавец'].fillna('Неизвестно', inplace=True)

# 3. Обрабатываем ошибочные значения
print("\nПроверка на отрицательные цены:")
print(df[df['цена'] < 0])

# Исправляем отрицательные цены (заменяем на медианное значение для соответствующего товара)
neg_price_indices = df[df['цена'] < 0].index for idx in neg_price_indices: product = df.loc[idx, 'товар'] median_price = df[df['товар'] == product]['цена'].median() df.loc[idx, 'цена'] = median_price if median_price > 0 else df['цена'].median()

# 4. Удаляем дубликаты
duplicate_count = df.duplicated().sum()
print(f"\nКоличество дубликатов: {duplicate_count}")
if duplicate_count > 0:
    df.drop_duplicates(inplace=True)
    print(f"Размер после удаления дубликатов: {df.shape}")

# 5. Преобразуем типы данных
# Преобразуем столбец даты в datetime
df['дата'] = pd.to_datetime(df['дата'])

# 6. Создаем дополнительные признаки для анализа
df['месяц'] = df['дата'].dt.month
df['день_недели'] = df['дата'].dt.day_name()
df['выручка'] = df['количество'] * df['цена']

print("\nDataFrame после предварительной обработки:")
print(df.head())
print("\nОбновленная информация:")
print(df.info())

# Проверяем результаты очистки
print("\nПроверка на отрицательные цены после обработки:")
print(df[df['цена'] < 0])
print("\nПроверка на пропущенные значения после обработки:")
print(df.isnull().sum())
Размер DataFrame: (105, 6)

Первые 5 строк:
         дата     товар        регион   продавец  количество      цена
0  2023-01-01  Наушники   Новосибирск     Петров         3.0  144000.0
1  2023-01-02   Ноутбук        Москва    Иванова         8.0   68000.0
2  2023-01-03  Наушники        Москва    Иванова         6.0  125000.0
3  2023-01-04   Планшет  Екатеринбург  Кузнецова         7.0   49000.0
4  2023-01-05   Телефон  Екатеринбург  Кузнецова         8.0   26000.0

Информация о DataFrame:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 105 entries, 0 to 104
Data columns (total 6 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   дата        105 non-null    object 
 1   товар       105 non-null    object 
 2   регион      105 non-null    object 
 3   продавец    102 non-null    object 
 4   количество  99 non-null     float64
 5   цена        105 non-null    float64
dtypes: float64(2), object(4)
memory usage: 5.1+ KB
None

Статистика по числовым столбцам:
       количество           цена
count   99.000000     105.000000
mean     5.434343   73552.380952
std      2.699817   46919.936093
min      1.000000   -1000.000000
25%      3.000000   30000.000000
50%      6.000000   72000.000000
75%      8.000000  118000.000000
max      9.000000  149000.000000

Количество пропущенных значений по столбцам:
дата          0
товар         0
регион        0
продавец      3
количество    6
цена          0
dtype: int64

Проверка на отрицательные цены:
          дата     товар       регион продавец  количество    цена
64  2023-03-06  Наушники  Новосибирск  Сидоров         2.0 -1000.0
87  2023-03-29   Ноутбук       Москва   Петров         7.0 -1000.0

Количество дубликатов: 5
Размер после удаления дубликатов: (100, 6)

DataFrame после предварительной обработки:
        дата     товар        регион   продавец  количество      цена  месяц  \
0 2023-01-01  Наушники   Новосибирск     Петров         3.0  144000.0      1   
1 2023-01-02   Ноутбук        Москва    Иванова         8.0   68000.0      1   
2 2023-01-03  Наушники        Москва    Иванова         6.0  125000.0      1   
3 2023-01-04   Планшет  Екатеринбург  Кузнецова         7.0   49000.0      1   
4 2023-01-05   Телефон  Екатеринбург  Кузнецова         8.0   26000.0      1   

  день_недели   выручка  
0      Sunday  432000.0  
1      Monday  544000.0  
2     Tuesday  750000.0  
3   Wednesday  343000.0  
4    Thursday  208000.0  

Обновленная информация:
<class 'pandas.core.frame.DataFrame'>
Index: 100 entries, 0 to 99
Data columns (total 9 columns):
 #   Column       Non-Null Count  Dtype         
---  ------       --------------  -----         
 0   дата         100 non-null    datetime64[ns]
 1   товар        100 non-null    object        
 2   регион       100 non-null    object        
 3   продавец     100 non-null    object        
 4   количество   100 non-null    float64       
 5   цена         100 non-null    float64       
 6   месяц        100 non-null    int32         
 7   день_недели  100 non-null    object        
 8   выручка      100 non-null    float64       
dtypes: datetime64[ns](1), float64(3), int32(1), object(4)
memory usage: 7.4+ KB
None

Проверка на отрицательные цены после обработки:
Empty DataFrame
Columns: [дата, товар, регион, продавец, количество, цена, месяц, день_недели, выручка]
Index: []

Проверка на пропущенные значения после обработки:
дата           0
товар          0
регион         0
продавец       0
количество     0
цена           0
месяц          0
день_недели    0
выручка        0
dtype: int64

Этот пример демонстрирует типичный workflow предварительной обработки данных, включающий:

  1. Загрузку данных из файла;
  2. Изучение структуры и базовых статистик;
  3. Обработку пропущенных значений;
  4. Исправление ошибочных данных;
  5. Удаление дубликатов;
  6. Преобразование типов данных;
  7. Создание дополнительных признаков для анализа.
Читайте также:  Как предсказать отток клиентов с помощью машинного обучения

В реальных проектах этап предварительной обработки может занимать до 80% времени всего процесса анализа данных, но инвестиции в качественную подготовку данных окупаются повышением точности и надежности последующего анализа.

Группировка и агрегация данных

Группировка и агрегация – это мощные инструменты для выявления паттернов в данных. Pandas предоставляет гибкий интерфейс для этих операций через метод groupby(), который позволяет разбивать данные на группы и применять к ним различные функции.

Рассмотрим различные типы группировок и агрегаций на примере данных о продажах:

import pandas as pd
import numpy as np

# Используем DataFrame из предыдущего примера
# Если его нет, создадим новый набор данных
try:
    df
except NameError:
    # Создаем данные о продажах
    np.random.seed(42)  # Для воспроизводимости результатов
    df = pd.DataFrame({
        'дата': pd.date_range(start='2023-01-01', periods=1000),
        'товар': np.random.choice(['Телефон', 'Ноутбук', 'Планшет', 'Наушники'], 1000),
        'регион': np.random.choice(['Москва', 'Санкт-Петербург', 'Новосибирск', 'Екатеринбург'], 1000),
        'продавец': np.random.choice(['Иванова', 'Петров', 'Сидоров', 'Кузнецова'], 1000),
        'количество': np.random.randint(1, 10, 1000),
        'цена': [round(np.random.uniform(5000, 150000), -3) for _ in range(1000)]
    })
    df['месяц'] = df['дата'].dt.month
    df['день_недели'] = df['дата'].dt.day_name()
    df['выручка'] = df['количество'] * df['цена']

# 1. Базовая группировка по одному столбцу
print("Средняя выручка по товарам:")
print(df.groupby('товар')['выручка'].mean())

# 2. Группировка по нескольким столбцам
print("\nСредняя выручка по товарам и регионам:")
print(df.groupby(['товар', 'регион'])['выручка'].mean())

# 3. Несколько агрегирующих функций
print("\nРазличные статистики по товарам:")
print(df.groupby('товар')['выручка'].agg(['count', 'min', 'max', 'mean', 'std']))

# 4. Разные функции для разных столбцов
agg_funcs = {
    'количество': ['sum', 'mean'],
    'цена': ['min', 'max', 'mean'],
    'выручка': ['sum', 'mean', 'std']
}
print("\nКомплексная агрегация по товарам:")
print(df.groupby('товар').agg(agg_funcs))

# 5. Пользовательские агрегирующие функции
def revenue_to_quantity_ratio(x):
    """Отношение выручки к количеству проданных товаров"""
    return x['выручка'].sum() / x['количество'].sum()

def top_sale(x):
    """Максимальная единичная продажа"""
    return (x['количество'] * x['цена']).max()

print("\nПользовательские агрегации:")
print(df.groupby('товар').apply(revenue_to_quantity_ratio))
print("\nМаксимальная единичная продажа по продавцам:")
print(df.groupby('продавец').apply(top_sale))

# 6. Группировка с перечислением (transform)
# Добавим столбец с информацией о средней цене товара в данном регионе
df['средняя_цена_в_регионе'] = df.groupby('регион')['цена'].transform('mean')
print("\nДобавлен столбец со средней ценой в регионе:")
print(df[['регион', 'товар', 'цена', 'средняя_цена_в_регионе']].head(10))

# 7. Фильтрация групп
# Отберем только те товары, средняя выручка по которым превышает 300,000
high_revenue_products = df.groupby('товар').filter(lambda x: x['выручка'].mean() > 300000)
print(f"\nТовары с высокой средней выручкой (>300,000): {high_revenue_products['товар'].unique()}")

# 8. Группировка по временным интервалам
print("\nСредняя выручка по месяцам:")
print(df.groupby('месяц')['выручка'].mean())

print("\nСредняя выручка по дням недели:")
print(df.groupby('день_недели')['выручка'].mean())
Средняя выручка по товарам:
товар
Наушники    396782.608696
Ноутбук     408035.714286
Планшет     398280.000000
Телефон     473375.000000
Name: выручка, dtype: float64

Средняя выручка по товарам и регионам:
товар     регион         
Наушники  Екатеринбург       359500.000000
          Москва             711500.000000
          Новосибирск        150000.000000
          Санкт-Петербург    588500.000000
Ноутбук   Екатеринбург       531500.000000
          Москва             268300.000000
          Новосибирск        487142.857143
          Санкт-Петербург    428600.000000
Планшет   Екатеринбург       186800.000000
          Москва             738714.285714
          Новосибирск        407285.714286
          Санкт-Петербург    166833.333333
Телефон   Екатеринбург       611636.363636
          Москва             435125.000000
          Новосибирск        288000.000000
          Санкт-Петербург    192000.000000
Name: выручка, dtype: float64

Различные статистики по товарам:
          count      min        max           mean            std
товар                                                            
Наушники     23  36000.0  1043000.0  396782.608696  324270.784225
Ноутбук      28  24000.0  1128000.0  408035.714286  343109.256127
Планшет      25  23000.0  1251000.0  398280.000000  423709.857489
Телефон      24  18000.0  1260000.0  473375.000000  345670.213844

Комплексная агрегация по товарам:
         количество              цена                             выручка  \
                sum      mean     min       max          mean         sum   
товар                                                                       
Наушники      116.0  5.043478  6000.0  149000.0  82630.434783   9126000.0   
Ноутбук       155.0  5.535714  8000.0  141000.0  72678.571429  11425000.0   
Планшет       131.0  5.240000  8000.0  141000.0  65160.000000   9957000.0   
Телефон       137.0  5.708333  6000.0  147000.0  84458.333333  11361000.0   

                                        
                   mean            std  
товар                                   
Наушники  396782.608696  324270.784225  
Ноутбук   408035.714286  343109.256127  
Планшет   398280.000000  423709.857489  
Телефон   473375.000000  345670.213844  

Пользовательские агрегации:
товар
Наушники    78672.413793
Ноутбук     73709.677419
Планшет     76007.633588
Телефон     82927.007299
dtype: float64

Максимальная единичная продажа по продавцам:
продавец
Иванова       1197000.0
Кузнецова     1008000.0
Неизвестно     405000.0
Петров        1251000.0
Сидоров       1260000.0
dtype: float64

Добавлен столбец со средней ценой в регионе:
            регион     товар      цена  средняя_цена_в_регионе
0      Новосибирск  Наушники  144000.0            68152.173913
1           Москва   Ноутбук   68000.0            78931.034483
2           Москва  Наушники  125000.0            78931.034483
3     Екатеринбург   Планшет   49000.0            84066.666667
4     Екатеринбург   Телефон   26000.0            84066.666667
5  Санкт-Петербург   Телефон   50000.0            67388.888889
6           Москва   Ноутбук   71000.0            78931.034483
7      Новосибирск  Наушники    8000.0            68152.173913
8           Москва   Телефон   26000.0            78931.034483
9     Екатеринбург   Телефон   37000.0            84066.666667

Товары с высокой средней выручкой (>300,000): ['Наушники' 'Ноутбук' 'Планшет' 'Телефон']

Средняя выручка по месяцам:
месяц
1    403935.483871
2    382428.571429
3    439612.903226
4    501100.000000
Name: выручка, dtype: float64

Средняя выручка по дням недели:
день_недели
Friday       445285.714286
Monday       513200.000000
Saturday     330142.857143
Sunday       324800.000000
Thursday     406071.428571
Tuesday      519642.857143
Wednesday    391642.857143
Name: выручка, dtype: float64

Группировка и агрегация позволяют сжать большие объемы данных до нескольких информативных показателей, что значительно упрощает понимание общих тенденций и закономерностей. В своей практике я часто комбинирую группировку с визуализацией для наглядного представления результатов анализа.

Работа с временными рядами

Pandas имеет мощную поддержку для работы с временными рядами, что делает его незаменимым инструментом для анализа данных с временной составляющей. Я часто использую эти возможности при анализе сезонных трендов, финансовых данных и других временных последовательностей.

Рассмотрим основные методы анализа временных рядов:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Создаем временной ряд для демонстрации
# Будем моделировать ежедневные продажи с трендом, сезонностью и случайным шумом
np.random.seed(42)

# Генерируем временной индекс
date_range = pd.date_range('2023-01-01', '2023-12-31', freq='D')

# Базовый тренд - линейный рост
trend = np.linspace(100, 200, len(date_range)) 

# Сезонность - годовая и недельная
annual_seasonality = 50 * np.sin(2 * np.pi * np.arange(len(date_range)) / 365)
weekly_seasonality = 30 * (np.arange(len(date_range)) % 7 < 2)  # Выходные дни

# Добавляем случайный шум
noise = np.random.normal(0, 5, len(date_range))

# Объединяем компоненты
sales = trend + annual_seasonality + weekly_seasonality + noise

# Создаем Series с временным индексом
sales_ts = pd.Series(sales, index=date_range, name='продажи')

# Визуализируем исходный временной ряд
plt.figure(figsize=(12, 6))
sales_ts.plot()
plt.title('Ежедневные продажи за 2023 гг.')
plt.ylabel('Объем продаж')
plt.grid(True)
plt.show()

# 1. Ресемплинг - изменение частоты временного ряда
# Месячный ресемплинг
monthly_sales = sales_ts.resample('M').mean()
print("Средние месячные продажи:")
print(monthly_sales.head())

# Недельный ресемплинг
weekly_sales = sales_ts.resample('W').sum()
print("\nНедельные суммы продаж:")
print(weekly_sales.head())

# 2. Скользящие средние для сглаживания
# 7-дневное скользящее среднее
sales_ma7 = sales_ts.rolling(window=7).mean()
# 30-дневное скользящее среднее
sales_ma30 = sales_ts.rolling(window=30).mean()

plt.figure(figsize=(12, 6))
sales_ts.plot(alpha=0.5, label='Исходные данные')
sales_ma7.plot(label='7-дневное скользящее среднее')
sales_ma30.plot(label='30-дневное скользящее среднее')
plt.title('Сглаживание временного ряда скользящими средними')
plt.legend()
plt.grid(True)
plt.show()

# 3. Сдвиг данных (lag) для анализа зависимостей
# Вычисляем изменение относительно предыдущего дня
sales_ts_diff = sales_ts.diff()
# Вычисляем изменение относительно предыдущей недели
sales_ts_diff_7 = sales_ts.diff(7)

plt.figure(figsize=(12, 8))
plt.subplot(2, 1, 1)
sales_ts_diff.plot()
plt.title('Изменение продаж относительно предыдущего дня')
plt.grid(True)

plt.subplot(2, 1, 2)
sales_ts_diff_7.plot()
plt.title('Изменение продаж относительно предыдущей недели')
plt.grid(True)

plt.tight_layout()
plt.show()

# 4. Анализ сезонности по дням недели
sales_by_weekday = sales_ts.groupby(sales_ts.index.dayofweek).mean()
days = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье']
sales_by_weekday.index = days

plt.figure(figsize=(10, 5))
sales_by_weekday.plot(kind='bar')
plt.title('Средние продажи по дням недели')
plt.ylabel('Средний объем продаж')
plt.grid(True, axis='y')
plt.show()

# 5. Анализ сезонности по месяцам
sales_by_month = sales_ts.groupby(sales_ts.index.month).mean()
months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек']
sales_by_month.index = months

plt.figure(figsize=(10, 5))
sales_by_month.plot(kind='bar')
plt.title('Средние продажи по месяцам')
plt.ylabel('Средний объем продаж')
plt.grid(True, axis='y')
plt.show()

# 6. Автокорреляция для выявления периодичности
from pandas.plotting import autocorrelation_plot

plt.figure(figsize=(12, 6))
autocorrelation_plot(sales_ts)
plt.title('Автокорреляционная функция продаж')
plt.grid(True)
plt.show()

График ежедневных продаж за 2023 г

Рис. 1: График ежедневных продаж за 2023 г

Сглаживание временного ряда скользящими средними

Рис. 2: Сглаживание временного ряда скользящими средними

Графики изменения продаж относительно предыдущего дня / предыдущей недели

Рис. 3: Графики изменения продаж относительно предыдущего дня / предыдущей недели

Средние месячные продажи:
2023-01-31    125.407725
2023-02-28    154.410769
2023-03-31    175.538328
2023-04-30    185.773726
2023-05-31    181.425549
Freq: ME, Name: продажи, dtype: float64

Недельные суммы продаж:
2023-01-01    132.483571
2023-01-08    811.312944
2023-01-15    825.732669
2023-01-22    889.567941
2023-01-29    938.175089
Freq: W-SUN, Name: продажи, dtype: float64

Средний объем продаж по дням недели

Рис. 4: Средний объем продаж по дням недели

Средний объем продаж по месяцам

Рис. 5: Средний объем продаж по месяцам

Автокорреляционная функция динамики объема продаж

Рис. 6: Автокорреляционная функция динамики объема продаж

Анализ временных рядов особенно важен для бизнес-аналитики, где понимание сезонных паттернов и долгосрочных трендов может дать значительное конкурентное преимущество. Pandas с его удобным API для работы с датами и временем существенно упрощает эту задачу.

Читайте также:  Как находить точки роста онлайн-бизнеса с помощью Python

Разбиение данных на категории с помощью pd.cut и квантилей

В процессе дескриптивного анализа часто возникает необходимость разбить непрерывные данные на категории или группы. Pandas предоставляет для этого несколько удобных функций, в частности, pd.cut и pd.qcut. Их отличия в следующем:

  • pd.cut разбивает данные на категории по заданным интервалам;
  • pd.qcut использует квантили для создания групп с примерно равным количеством наблюдений.

Оба метода чрезвычайно полезны для бинирования данных и последующего анализа.

Рассмотрим практические примеры использования этих функций:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Создаем датасет для демонстрации
np.random.seed(42)
data = {
    'возраст': np.random.normal(35, 10, 1000).clip(18, 70),  # Возраст от 18 до 70
    'доход': np.random.lognormal(10, 0.6, 1000),  # Логнормальное распределение для дохода
    'стаж': np.random.exponential(10, 1000).clip(0, 40),  # Стаж работы от 0 до 40 лет
    'расходы': np.random.gamma(shape=5, scale=20000, size=1000)  # Гамма-распределение для расходов
}

df = pd.DataFrame(data)

# 1. Разбиение на фиксированные интервалы с помощью pd.cut
# Разбиваем возраст на 5 равных интервалов
df['возрастная_группа'] = pd.cut(df['возраст'], bins=5)
print("Разбиение возраста на 5 равных интервалов:")
print(df['возрастная_группа'].value_counts().sort_index())

# Визуализация распределения
plt.figure(figsize=(10, 6))
sns.countplot(x='возрастная_группа', data=df)
plt.title('Распределение по возрастным группам (равные интервалы)')
plt.xlabel('Возрастная группа')
plt.ylabel('Количество')
plt.xticks(rotation=45)
plt.grid(True, axis='y')
plt.tight_layout()
plt.show()

# 2. Разбиение с пользовательскими интервалами
custom_bins = [18, 25, 35, 50, 70]
custom_labels = ['18-25', '26-35', '36-50', '51-70']
df['возрастная_категория'] = pd.cut(df['возраст'], bins=custom_bins, labels=custom_labels)

print("\nРазбиение возраста на пользовательские интервалы:")
print(df['возрастная_категория'].value_counts().sort_index())

# Визуализация
plt.figure(figsize=(10, 6))
sns.countplot(x='возрастная_категория', data=df)
plt.title('Распределение по возрастным категориям (пользовательские интервалы)')
plt.xlabel('Возрастная категория')
plt.ylabel('Количество')
plt.grid(True, axis='y')
plt.tight_layout()
plt.show()

# 3. Разбиение на равные группы с помощью pd.qcut (квантили)
# Разбиваем доход на квартили (4 равные группы)
df['доход_квартиль'] = pd.qcut(df['доход'], q=4)
print("\nРазбиение дохода на квартили:")
print(df['доход_квартиль'].value_counts().sort_index())

# Разбиваем доход на децили (10 равных групп)
df['доход_дециль'] = pd.qcut(df['доход'], q=10, labels=False)  # labels=False возвращает номера групп (0-9)
print("\nРазбиение дохода на децили (количество наблюдений в каждой группе):")
print(df['доход_дециль'].value_counts().sort_index())

# Визуализация средних расходов по группам дохода
income_expense = df.groupby('доход_дециль')['расходы'].mean().reset_index()
plt.figure(figsize=(12, 6))
sns.barplot(x='доход_дециль', y='расходы', data=income_expense)
plt.title('Средние расходы по децилям дохода')
plt.xlabel('Дециль дохода (0 - низший, 9 - высший)')
plt.ylabel('Средние расходы')
plt.grid(True, axis='y')
plt.tight_layout()
plt.show()

# 4. Комбинирование qcut и группировки для сложного анализа
# Разбиваем на группы по возрасту и стажу и анализируем средний доход
df['возраст_группа'] = pd.qcut(df['возраст'], q=3, labels=['молодой', 'средний', 'старший'])
df['стаж_группа'] = pd.qcut(df['стаж'], q=3, labels=['начинающий', 'опытный', 'эксперт'])

# Создаем сводную таблицу средних доходов по группам
income_by_age_experience = df.pivot_table(
    values='доход', 
    index='возраст_группа', 
    columns='стаж_группа', 
    aggfunc='mean'
)

print("\nСредний доход по группам возраста и стажа:")
print(income_by_age_experience)

# Визуализация в виде тепловой карты
plt.figure(figsize=(10, 8))
sns.heatmap(income_by_age_experience, annot=True, fmt='.0f', cmap='YlGnBu')
plt.title('Средний доход по группам возраста и стажа')
plt.tight_layout()
plt.show()

# 5. Использование cut и qcut для детальной визуализации распределения
plt.figure(figsize=(12, 10))

# Распределение дохода с цветовым кодированием по возрастным группам
plt.subplot(2, 1, 1)
for category in df['возрастная_категория'].unique():
    subset = df[df['возрастная_категория'] == category]
    sns.kdeplot(subset['доход'], label=category)
plt.title('Распределение дохода по возрастным категориям')
plt.xlabel('Доход')
plt.ylabel('Плотность')
plt.legend()
plt.grid(True)

# Распределение стажа с цветовым кодированием по квартилям дохода
plt.subplot(2, 1, 2)
for quartile in sorted(df['доход_квартиль'].unique()):
    subset = df[df['доход_квартиль'] == quartile]
    sns.kdeplot(subset['стаж'], label=str(quartile))
plt.title('Распределение стажа по квартилям дохода')
plt.xlabel('Стаж (лет)')
plt.ylabel('Плотность')
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()
Разбиение возраста на 5 равных интервалов:
возрастная_группа
(17.948, 28.4]    245
(28.4, 38.8]      412
(38.8, 49.2]      254
(49.2, 59.6]       81
(59.6, 70.0]        8
Name: count, dtype: int64

Распределение данных по равным интервалам по возрастным группам

Рис. 7: Распределение данных по равным интервалам по возрастным группам

Разбиение возраста на пользовательские интервалы:
возрастная_категория
18-25    112
26-35    343
36-50    435
51-70     75
Name: count, dtype: int64

Разбиение данных на кастомные интервалы по возрастным категориям

Рис. 8: Разбиение данных на кастомные интервалы по возрастным категориям

Разбиение дохода на квартили:
доход_квартиль
(3773.531, 15309.903]      250
(15309.903, 22876.069]     250
(22876.069, 34109.464]     250
(34109.464, 149621.573]    250
Name: count, dtype: int64

Разбиение дохода на децили (количество наблюдений в каждой группе):
доход_дециль
0    100
1    100
2    100
3    100
4    100
5    100
6    100
7    100
8    100
9    100
Name: count, dtype: int64

Разбиение данных на децили

Рис. 9: Разбиение данных на децили

Средний доход по группам возраста и стажа:
стаж_группа       начинающий       опытный       эксперт
возраст_группа                                          
молодой         27004.098964  29092.116224  29798.127605
средний         24062.017262  26617.586429  27431.922075
старший         28273.014568  27177.327723  27359.578718

Тепловая карта совпадений среднего дохода по группам возраста и стажа

Рис. 10: Тепловая карта совпадений среднего дохода по группам возраста и стажа

Распределение дохода по возрастным категориям и Распределение стажа по квартилям дохода

Рис. 11: Распределение дохода по возрастным категориям и распределение стажа по квартилям дохода

Использование pd.cut и pd.qcut открывает широкие возможности для категоризации данных и последующего анализа взаимосвязей между различными переменными. Особенно полезны эти методы при подготовке данных для визуализации и создания понятных бизнес-отчетов, где важно представить информацию в агрегированной форме.

Анализ взаимосвязей между переменными

После изучения распределений отдельных переменных следующим логическим шагом является анализ их взаимосвязей. Pandas и NumPy предоставляют различные инструменты для такого анализа, от простых корреляций до более сложных методов.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats

# Используем данные из предыдущего примера или создаем новый датасет
try:
    df
except NameError:
    np.random.seed(42)
    df = pd.DataFrame({
        'возраст': np.random.normal(35, 10, 1000).clip(18, 70),
        'доход': np.random.lognormal(10, 0.6, 1000),
        'стаж': np.random.exponential(10, 1000).clip(0, 40),
        'расходы': np.random.gamma(shape=5, scale=20000, size=1000),
        'образование': np.random.choice(
            ['Среднее', 'Бакалавр', 'Магистр', 'Кандидат наук', 'Доктор наук'],
            size=1000,
            p=[0.2, 0.4, 0.3, 0.08, 0.02]
        )
    })
    
# 1. Диаграммы рассеяния (Scatter plots)
# Создаем матрицу диаграмм рассеяния
plt.figure(figsize=(16, 12))
sns.pairplot(df.select_dtypes(include=[np.number]), diag_kind='kde')
plt.suptitle('Матрица диаграмм рассеяния', y=1.02, fontsize=16)
plt.show()

# 2. Выборочная диаграмма рассеяния с линией регрессии
plt.figure(figsize=(10, 6))
sns.regplot(x='стаж', y='доход', data=df, scatter_kws={'alpha':0.5})
plt.title('Зависимость дохода от стажа с линией регрессии')
plt.grid(True)
plt.show()

# 3. Корреляционный анализ
# Коэффициент корреляции Пирсона (линейная зависимость)
pearson_corr = df.select_dtypes(include=[np.number]).corr(method='pearson')
print("Корреляция Пирсона:")
print(pearson_corr)

# Коэффициент ранговой корреляции Спирмена (монотонная зависимость)
spearman_corr = df.select_dtypes(include=[np.number]).corr(method='spearman')
print("\nКорреляция Спирмена:")
print(spearman_corr)

# Визуализация различий между корреляциями Пирсона и Спирмена
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

sns.heatmap(pearson_corr, annot=True, cmap='coolwarm', vmin=-1, vmax=1, ax=axes[0])
axes[0].set_title('Корреляция Пирсона')

sns.heatmap(spearman_corr, annot=True, cmap='coolwarm', vmin=-1, vmax=1, ax=axes[1])
axes[1].set_title('Корреляция Спирмена')

plt.tight_layout()
plt.show()

Матрица диаграмм рассеяния

Рис. 12: Матрица диаграмм рассеяния

Зависимость дохода от стажа с линией регрессии

Рис. 13: Зависимость дохода от стажа с линией регрессии

Корреляция Пирсона:
               возраст     доход      стаж   расходы  доход_дециль
возраст       1.000000 -0.027888  0.048552  0.040649     -0.060214
доход        -0.027888  1.000000  0.008092 -0.020345      0.863737
стаж          0.048552  0.008092  1.000000  0.014937      0.020211
расходы       0.040649 -0.020345  0.014937  1.000000     -0.008137
доход_дециль -0.060214  0.863737  0.020211 -0.008137      1.000000

Корреляция Спирмена:
               возраст     доход      стаж   расходы  доход_дециль
возраст       1.000000 -0.063610  0.047525  0.035456     -0.066834
доход        -0.063610  1.000000  0.027145 -0.023067      0.994988
стаж          0.047525  0.027145  1.000000  0.035572      0.025987
расходы       0.035456 -0.023067  0.035572  1.000000     -0.020330
доход_дециль -0.066834  0.994988  0.025987 -0.020330      1.000000

Матрицы корреляций по Пирсону и Спирмену

Рис. 14: Матрицы корреляций по Пирсону и Спирмену

Заключение

В этой статье мы рассмотрели ключевые возможности библиотек NumPy и Pandas, которые делают Python мощным инструментом для дескриптивного анализа данных.

NumPy предоставляет высокоэффективную реализацию массивов (ndarray) и оптимизированных функций для работы с ними. Он особенно полезен, когда требуется высокая производительность и точный контроль над памятью.

Pandas же расширяет эти возможности, предоставляя удобные структуры данных (Series, DataFrame) и богатый набор методов для анализа, агрегации и визуализации табличных данных. Особенно ценны его средства работы с пропущенными данными, временными рядами и категориальными признаками.

Мы рассмотрели такие важные техники, как:

  • Базовая статистика (медиана, мода, асимметрия, эксцесс);
  • Корреляционный анализ;
  • Группировка и агрегация данных;
  • Работа с временными рядами и сезонностью;
  • Разбиение данных на интервалы и квантили;
  • Визуализация зависимостей между переменными.

Особое внимание было уделено взаимосвязям между переменными, поскольку именно они позволяют выявлять скрытые закономерности и строить прогнозные модели. Мы использовали диаграммы рассеяния, коэффициенты корреляции Пирсона и Спирмена, а также визуализировали результаты с помощью тепловых карт и графиков.

На практике комбинация NumPy и Pandas становится основой для любого серьезного анализа данных. Эти библиотеки обеспечивают не только гибкость и скорость обработки, но и глубокую интеграцию с другими популярными инструментами экосистемы Python, такими как Matplotlib, Seaborn, Scikit-learn и Statsmodels.