Время прочтения: 8 мин.

Прежде чем разобраться импортируем необходимые библиотеки:

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

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

Всё пространство (figure) включает в себя график (Axes), который, в свою очередь, состоит из таких элементов, как границы (Spines), сетки (Grid), описания (Legend), самого графика (Line) и других.

Графики отражают зависимость, при этом элементы графика — оси, подписи и др. — открыты к настройке.

Базовый элемент figure может содержать в себе как один график, так и несколько.

Например, создание нескольких графиков в одном окне может быть выполнено как:

fig, axes = plt.subplots(
    1,               #одной строкой                                           
    3,               #три графика                                       
    figsize=(15, 4)  #вписать в размер 15х4 дюйма                           
)

или…

fig = plt.figure(figsize=(16, 8))                          #задаем фигуру

ax = [None for _ in range(6)]

ax[0] = plt.subplot2grid(                                  #первый график сетки
    (3,4),                                                 #размер сетки = 3 ряда по 4 элемента
    (0,0),                                                 #локация первого элемента
    colspan=4
)

ax[1] = plt.subplot2grid((3,4), (1,0), colspan=1)          #аналогично задаем параметры остальных графиков
ax[2] = plt.subplot2grid((3,4), (1,1), colspan=1)
ax[3] = plt.subplot2grid((3,4), (1,2), colspan=1)
ax[4] = plt.subplot2grid((3,4), (1,3), colspan=1,rowspan=2) #этот график займет 1 колонку, но 2 ряда
ax[5] = plt.subplot2grid((3,4), (2,0), colspan=3)           

for num in range(6): 
    ax[num].set_title(f'ax[{num}]')
    ax[num].set_xticks([])                                   #подпишем каждый
    ax[num].set_yticks([])                                   #..и уберем подписи осей

или…

fig = plt.figure(figsize=(16, 8))

gs = fig.add_gridspec(3, 4)           #задаем сетку 3х4                               

ax = [None for _ in range(6)]

ax[0] = fig.add_subplot(gs[0, :])     #для каждого графика задаем позицию и размер                              
ax[0].set_title('gs[0, :]')           #через элементы сетки                               
ax[1] = fig.add_subplot(gs[1, 0])
ax[1].set_title('gs[1, 0]')

ax[2] = fig.add_subplot(gs[1, 1])
ax[2].set_title('gs[1, 1]')

ax[3] = fig.add_subplot(gs[1, 2])
ax[3].set_title('gs[1, 2]')

ax[4] = fig.add_subplot(gs[1:, -1])
ax[4].set_title('gs[1:, -1]')

ax[5] = fig.add_subplot(gs[-1, :-1])
ax[5].set_title('gs[1, :-1]')

for num in range(6):
    ax[num].set_xticks([])
    ax[num].set_yticks([])

…или отрисовывать однотипные графики циклом/функцией (об этом ниже). Title, yticks, xticks, axes есть, поработаем над визуальным оформлением на реальных данных. В качестве них будет выступать информация о недвижимости на вторичном рынке.

df = pd.read_csv('df_filtered.csv', index_col=0)
df.head()
print(f"{df.shape[0]} объектов, {df.shape[1]} признаков \
\nКатегориальные признаки: \n{list(df.select_dtypes(include=['object']).columns)}")
4460 объектов, 15 признаков 
Категориальные признаки: 
['Flat_type', 'District', 'Street', 'Seller_category', 'Sellers_name', 'Photos_count']

Как мы видим, в таблице содержится информация о почти 4,5 тыс. объектов недвижимости, категориальные признаки: Тип объекта недвижимости, Район, Улица, Категория продавца, Количество фотографий.

Я хочу посмотреть на графики распределений для типа жилья, района и категории продавца, отрисуем сразу все

cat_data = df.select_dtypes(include=['object']).columns[[0, 1, -3]]  #выбираем колонки с типом жилья, районом и продавцом 

fig, ax = plt.subplots(1, 3, figsize=(15, 5)) #создаем фигуру 'fig' размером 15х5 дюймов
#в ней будет 1 ряд из 3х осей (графиков) 'ах'
                                                                                          
for num, col in enumerate(cat_data):          #отрисовываем в цикле, даем название
    sns.countplot(data = df, x = col, ax = ax[num])    
    ax[num].set_title(f'{col} distribution')                        

Первая проблема — все районы слились в один. Вторая — то же произошло с типом жилья. Можно повернуть подписи:

ax = sns.countplot(data = df, x = 'District')    
ax.set_title('District distribution')                                       
ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha='right')  #поворачиваем подписи  

…или графики, передав ось X на Y:

fig, ax = plt.subplots(3, 1, figsize=(15, 15))   #сделаем 3 ряда по 1 графику, чтобы они не мешали друг другу 

for num, col in enumerate(cat_data):
    sns.countplot(
        data = df, 
        y = col, 
        ax = ax[num],
        order = df[col].value_counts().index     #передадим порядок сортировки  
    )    
    ax[num].set_title(f'{col} distribution')

Поиграемся немного со шрифтами и цветом:

sns.set_palette('viridis') #выбираем цветовую схему
sns.set_style('whitegrid') #и фон

Цветовую схему можно не только выбрать из предустановленных https://matplotlib.org/stable/tutorials/colors/colormaps.html, но и создать самостоятельно https://matplotlib.org/stable/tutorials/colors/colormap-manipulation.html

sns.set_style({                                                   
    'axes.facecolor': '.95',      #яркость фона в диапазоне [0-1]                                
    'axes.edgecolor':'.9'         #яркость рамок в диапазоне [0-1]                                
})
fig, ax = plt.subplots(3, 1, figsize=(15, 15), dpi = 500)     #увеличим разрешение    

for num, col in enumerate(cat_data):
    sns.countplot(
        data = df, 
        y = col, 
        ax = ax[num], 
        order = df[col].value_counts().index
    )
    
    ax[num].set(xlabel=None, ylabel=None)     #уберем подписи осей
    ax[num].set_title(f'{col} distribution',fontdict= {   #зададим размер, шрифт и положение заголовков         
        'fontsize': 18, 'fontweight':'bold', 'fontfamily':'serif' 
    }, loc = 'left')
    
    counts = df[col].value_counts()
    for i, j in enumerate(counts.index):                           
        ax[num].annotate(                             #подставим проценты            
        f'{counts[j] / counts.sum():.1%}',            #(первый аргумент - текст,            
        xy=(counts[j] + 0.008*counts[j], i),          #второй аргумент - его место на графике - х,у)
        va = 'center',
        fontweight='bold'
        )
        
plt.suptitle(                                           #...и комментарий         
    '*в отличии от категории продавца и типа жилья район может быть не заполнен',
    x=0.66, y=0.1, color='gray'
);      

Ок, мы увидели распределение, теперь настроим тепловую карту матрицы корреляции. Сначала с дефолтными параметрами:

Кстати, символ «;» позволяет избавиться от подписи объекта.

Чем светлее секция — тем выше линейная зависимость признаков. Цена сильно зависит от площади и количества комнат, что очевидно. Увеличим график и поиграем с цветами:

plt.figure(figsize=(10, 7))            #задаем размер                      
cmap = sns.diverging_palette(200, 5, as_cmap=True)    #цветовую палитру        
mask = np.zeros_like(df.corr(), dtype=bool)
mask[np.triu_indices_from(mask)] = True          #оставим половину карты (инфо дублируется)             

sns.heatmap(df.corr(), mask=mask, cmap=cmap);

Теперь посмотрим на плотность распределения цен:

sns.kdeplot(df['Price']);

В разрезе количества комнат:

rooms = sorted(df[df['Rooms_count'].notnull()]['Rooms_count'].unique())

fig, ax = plt.subplots(len(rooms), 1, figsize=(15, 15), dpi = 500)

for r, idx in zip(rooms, range(len(rooms))):
    sns.kdeplot(x = 'Price', data = df[df['Rooms_count'] == r], ax = ax[idx])
    ax[idx].set(xlabel=None, ylabel=None)
    ax[idx].set_title(f'Комнат: {int(r)}')

В глаза бросаются 2 вещи: несовпадение по оси Х и перекрытие названия снизу подписями по оси Х выше. Отрисуем заново через add_gridspec:

fig = plt.figure(figsize=(15, 15))
gs = fig.add_gridspec(len(rooms),1)
gs.update(hspace= -0.25)

axes = []

for idx, r in zip(range(len(rooms)), rooms):
    axes.append(fig.add_subplot(gs[idx, 0]))
    
    sns.kdeplot(x='Price', data=df[df['Rooms_count'] == r], #каждый график будет закрашен
                fill=True, ax=axes[idx], cut=0, bw_method=0.25, #контур светло-серый
                lw=1.4, edgecolor='lightgray', alpha=1) 
    
    axes[idx].set_xlim(0, max(df['Price']))#отмасштабируем графики по 
                                           #единой шкале Х
    axes[idx].set_xticks([])               #по осям не будет шкал         
    axes[idx].set_yticks([])
    axes[idx].set_ylabel('')               #и подписей
    axes[idx].set_xlabel('')
    
    spines = ['top','right','left','bottom'] #и границ
    for spine in spines:
        axes[idx].spines[spine].set_visible(False)
        
    axes[idx].patch.set_alpha(0)
    axes[idx].text(0,0,f'Комнат: {int(r)}', fontweight='bold', fontfamily='serif',
    fontsize=10, ha='right')

fig.text(0.125,0.89, 'Плотность распределения цены в разрезе количества комнат:', fontweight='bold', fontfamily='serif', fontsize=16);

В данном случае (мы смотрим на плотность распределения) нас интересует визуальная часть, поэтому абсолютными значениями и шкалами мы можем пренебречь:

Добавим градиентную заливку. Для этого выберем colormap (например, PuBuGn) и с помощью функции get_cmap обратимся к нему и скажем, сколько хотим цветов (в нашем случае по количеству вариантов rooms):

matplotlib.cm.get_cmap('PuBuGn', len(rooms))

С помощью rgb2hex нужно перевести полученные цвета (они в формате rgba) в необходимый нам формат (hex):

matplotlib.cm.get_cmap('PuBuGn', len(rooms))
colors = []

for i in range(cmap.N):
    rgb = cmap(i)[:3]
    colors.append(matplotlib.colors.rgb2hex(rgb))

…и добавить в список, элементы которого мы будем передавать в качестве аргумента при построении графика:

fig = plt.figure(figsize=(15, 15))
gs = fig.add_gridspec(len(rooms),1)
gs.update(hspace= -0.25)

axes = []

for idx, r, c in zip(range(len(rooms)), rooms, colors): #список цветов
    axes.append(fig.add_subplot(gs[idx, 0]))
    
    sns.kdeplot(x='Price', data=df[df['Rooms_count'] == r], 
                fill=True, ax=axes[idx], cut=0, bw_method=0.25, 
                lw=1.4, color=c, edgecolor='lightgray', alpha=1) #передаем элементы в функцию
    
    axes[idx].set_xlim(0, max(df['Price']))
    
    axes[idx].set_xticks([])
    axes[idx].set_yticks([])
    axes[idx].set_ylabel('')
    axes[idx].set_xlabel('')
    
    spines = ['top','right','left','bottom']
    for spine in spines:
        axes[idx].spines[spine].set_visible(False)
        
    axes[idx].patch.set_alpha(0)
    axes[idx].text(0,0,f'Комнат: {int(r)}', fontweight='bold', fontfamily='serif',
    fontsize=10, ha='right')

fig.text(0.125,0.89, 'Плотность распределения цены в разрезе количества комнат:', fontweight='bold', fontfamily='serif', fontsize=16);

Также стоит остановиться у функции text – она позволяет без привязки к осям, заголовку или графику прокомментировать элемент:

fig, ax = plt.subplots(figsize=(16, 8), dpi=500)   #фигура, размер, разрешение
                                   

ax.text(                                                                              
    0.1,  #позиция по оси Х                                                                            
    0.9,  #позиция по оси Y                                                                           
    'серый',   #текст                                                                            
    color='gray', #цвет                                                                   
    va='center',  #центрируем по вертикали                                                                        
    ha='center',  #и горизонтали                                                                    
    fontsize=18   #размер шрифта                                                                    
)

ax.text(0.3, 0.7, 'красный в рамке', color='red', va='center', ha='center', #аналогично + параметры рамки         
        bbox=dict(facecolor='none', edgecolor='red'), fontsize=18)

ax.text(0.5, 0.5, 'синий в рамке', color='blue', va='center', ha='center',
        bbox=dict(facecolor='none', edgecolor='blue', pad=10.0), fontsize=18)

ax.text(0.7, 0.3, 'зелёный в рамке с округлыми краями', color='green', va='center', ha='center',
        bbox=dict(facecolor='none', edgecolor='green', boxstyle='round'), fontsize=18)

ax.text(0.9, 0.1, 'и чёрный', color='white', va='center', ha='center',
        bbox=dict(facecolor='black', edgecolor='black', boxstyle='round', pad=0.5), fontsize=18)

ax.set_xticks([])
ax.set_yticks([])

Как итог — библиотека matplotlib позволяет строить графики любых, даже самых сложных форм, а впоследствии настраивать их так, чтобы добиться максимальной читаемости и информативности.

PS. В matplotlib есть встроенный модуль xkcd (в честь известного комикса) — настройка позволяет отрисовывать графики в скетч-стиле:

with plt.xkcd():
    fig = plt.figure(figsize=(10,7))
    ax = fig.add_axes((0.1, 0.2, 0.8, 0.7))
    ax.spines['right'].set_color('none')   #убираем рамку справа
    ax.spines['top'].set_color('none')     #и слева                                  
    ax.set_xticks([])                      #убираем подписи осей                                  
    ax.set_yticks([])

    x = [i for i in range(15)]
    y = [i**0.5 for i in x]

    ax.plot(x,y)
    
    ax.set_title('Learning Python:')
    ax.set_xlabel('for funny drawing')
    ax.set_ylabel('for being DS')