Практика · Графіки matplotlib¶

Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md

Наскрізний приклад той самий, що в лекції, — кавʼярня «Дві ложки»: шістдесят днів роботи й щоденний виторг. Дані тут не випадкові: вони рахуються за формулою, тому в тебе на екрані будуть точно ті самі числа, що в лекції.

Що зробимо:

  1. порахуємо статистики чотирьох наборів і доведемо assert-ом, що вони збігаються до другого знака — а потім намалюємо їх і побачимо чотири різні картини;
  2. згенеруємо дані кавʼярні й звіримо їх із числами з лекції;
  3. побудуємо чотири типи графіка на тих самих даних у сітці 2×2;
  4. підпишемо осі, заголовок і легенду — і порівняємо «до» й «після»;
  5. подивимось, що робить sharey=True і чому без нього два графіки поруч брешуть;
  6. збережемо картинку у файл із двома різними dpi й порівняємо розміри в байтах;
  7. намалюємо те саме через df.plot() і доробимо результат через ax;
  8. зробимо пару «обрізана вісь проти чесної» й порахуємо, у скільки разів змінюється враження.

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

In [1]:
# %matplotlib inline вмикає малювання просто у вивід клітинки, а не в окреме вікно
%matplotlib inline

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

# однаковий вигляд у всіх, хто запускає зошит: без цього розмір шрифту
# залежить від налаштувань конкретної машини
plt.rcParams["figure.figsize"] = (9, 4)
plt.rcParams["font.size"] = 11

print("Python     :", sys.version.split()[0])
print("numpy      :", np.__version__)
print("pandas     :", pd.__version__)
print("matplotlib :", matplotlib.__version__)
print("бекенд     :", matplotlib.get_backend())
Python     : 3.12.2
numpy      : 2.5.1
pandas     : 2.3.3
matplotlib : 3.11.1
бекенд     : module://matplotlib_inline.backend_inline

1 · Чотири набори з однаковими числами¶

Починаємо з того самого досліду, що й лекція. Чотири набори по одинадцять пар чисел: x — витрати на рекламу, y — виторг. Спершу порахуємо для кожного набору звичайні показники й не будемо дивитись на дані.

Кореляцію порахуємо двічі: спершу формулою руками, потім бібліотечною np.corrcoef — щоб побачити, що всередині бібліотеки немає магії.

In [2]:
x_123 = np.array([10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5], dtype=float)
x_4   = np.array([8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8], dtype=float)

набори = {
    "набір 1": (x_123, np.array([8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68])),
    "набір 2": (x_123, np.array([9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74])),
    "набір 3": (x_123, np.array([7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73])),
    "набір 4": (x_4,   np.array([6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50, 5.56, 7.91, 6.89])),
}

def кореляція_руками(x, y):
    """Коефіцієнт Пірсона за означенням: наскільки узгоджено обидві величини
    відхиляються від своїх середніх."""
    відхилення_x = x - x.mean()
    відхилення_y = y - y.mean()
    чисельник = (відхилення_x * відхилення_y).sum()
    знаменник = np.sqrt((відхилення_x ** 2).sum() * (відхилення_y ** 2).sum())
    return чисельник / знаменник

рядки = []
for назва, (x, y) in набори.items():
    нахил, зсув = np.polyfit(x, y, 1)          # пряма, що найкраще описує звʼязок
    рядки.append({
        "набір": назва,
        "середнє x": x.mean(),
        "середнє y": y.mean(),
        "відхилення y": y.std(ddof=1),
        "кореляція": кореляція_руками(x, y),
        "нахил": нахил,
        "зсув": зсув,
    })

показники = pd.DataFrame(рядки).set_index("набір").round(2)
print(показники)
         середнє x  середнє y  відхилення y  кореляція  нахил  зсув
набір                                                              
набір 1        9.0        7.5          2.03       0.82    0.5   3.0
набір 2        9.0        7.5          2.03       0.82    0.5   3.0
набір 3        9.0        7.5          2.03       0.82    0.5   3.0
набір 4        9.0        7.5          2.03       0.82    0.5   3.0
In [3]:
# перевірка «наша реалізація = бібліотечна»: формула руками проти np.corrcoef
for назва, (x, y) in набори.items():
    наше = кореляція_руками(x, y)
    бібліотечне = np.corrcoef(x, y)[0, 1]
    assert np.allclose(наше, бібліотечне), f"{назва}: розрахунок розійшовся!"
print("✅ наша формула кореляції збігається з np.corrcoef для всіх чотирьох наборів")

# а тепер головне: усі чотири рядки таблиці однакові до другого знака
перший_рядок = показники.iloc[0]
for назва in показники.index[1:]:
    assert (показники.loc[назва] == перший_рядок).all(), f"{назва} відрізняється від першого"
print("✅ усі чотири набори мають ОДНАКОВІ середні, відхилення, кореляцію й пряму")
print()
print("спільний рядок показників:")
print(перший_рядок)
✅ наша формула кореляції збігається з np.corrcoef для всіх чотирьох наборів
✅ усі чотири набори мають ОДНАКОВІ середні, відхилення, кореляцію й пряму

спільний рядок показників:
середнє x       9.00
середнє y       7.50
відхилення y    2.03
кореляція       0.82
нахил           0.50
зсув            3.00
Name: набір 1, dtype: float64

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

In [4]:
fig, axes = plt.subplots(1, 4, figsize=(13, 3.4), sharey=True, constrained_layout=True)

сітка_x = np.array([2, 21])                     # дві точки, щоб провести пряму через увесь графік
for ax, (назва, (x, y)) in zip(axes, набори.items()):
    ax.scatter(x, y, s=42, zorder=3)
    ax.plot(сітка_x, 0.5 * сітка_x + 3.0, linestyle="--", linewidth=1.4)
    ax.set_title(назва)
    ax.set_xlabel("витрати на рекламу")
    ax.set_xlim(2, 21)
    ax.set_ylim(2, 13.5)
    ax.grid(True, alpha=0.3)

axes[0].set_ylabel("виторг")
fig.suptitle("Однакові числа — різні форми", fontsize=13)
plt.show()

print("Чотири однакові рядки таблиці — і чотири різні картини.")
print("Перший: шум навколо прямої. Другий: дуга. Третій: пряма з викидом.")
print("Четвертий: звʼязку немає, всю кореляцію створює єдина точка при x = 19.")
No description has been provided for this image
Чотири однакові рядки таблиці — і чотири різні картини.
Перший: шум навколо прямої. Другий: дуга. Третій: пряма з викидом.
Четвертий: звʼязку немає, всю кореляцію створює єдина точка при x = 19.

2 · Дані кавʼярні¶

Далі всюди працюємо з одним набором: щоденний виторг кавʼярні за шістдесят днів. Формула детермінована, тому числа збігаються з тими, що в лекції, — це зараз і перевіримо.

In [5]:
дні = np.arange(60)                                   # 0 … 59

# псевдовипадковий, але відтворюваний шум: та сама формула, що в лекції
код = (дні * 1103515245 + 12345) % 2 ** 31
шум = код % 361 - 180                                 # від -180 до +180 грн

вихідний = np.isin(дні % 7, [5, 6])                   # субота й неділя людніші
виторг = 3800 + 6 * дні + вихідний * 1200 + шум       # +6 грн на день — повільне зростання

print("перші 10 днів :", виторг[:10])
print("усього днів   :", виторг.size)
print("середній виторг:", round(виторг.mean(), 1), "грн")
print("будні / вихідні:", (~вихідний).sum(), "/", вихідний.sum())

# звірка з числами, які надруковані в лекції
assert list(виторг[:5]) == [3691, 3673, 3956, 3938, 3860], "дані розійшлися з лекцією"
assert round(виторг.mean(), 1) == 4288.7
print("✅ дані збігаються з лекцією")
перші 10 днів : [3691 3673 3956 3938 3860 5042 4964 3746 3668 4011]
усього днів   : 60
середній виторг: 4288.7 грн
будні / вихідні: 44 / 16
✅ дані збігаються з лекцією
In [6]:
# середнє по всіх днях нічого не описує, бо груп насправді дві — перевіримо це числами
середнє_буднів = виторг[~вихідний].mean()
середнє_вихідних = виторг[вихідний].mean()

print(f"середнє по всіх днях : {виторг.mean():7.1f} грн")
print(f"тільки будні         : {середнє_буднів:7.1f} грн")
print(f"тільки вихідні       : {середнє_вихідних:7.1f} грн")
print(f"розрив між групами   : {середнє_вихідних - середнє_буднів:7.1f} грн")

# жоден день не має виторгу, близького до загального середнього ±50 грн?
близькі = np.abs(виторг - виторг.mean()) < 50
print("днів у смузі ±50 грн навколо загального середнього:", близькі.sum(), "із 60")
assert середнє_вихідних - середнє_буднів > 1000, "групи мали розійтись більш ніж на 1000 грн"
print("✅ загальне середнє лежить у порожнечі між двома групами")
середнє по всіх днях :  4288.7 грн
тільки будні         :  3984.8 грн
тільки вихідні       :  5124.4 грн
розрив між групами   :  1139.6 грн
днів у смузі ±50 грн навколо загального середнього: 1 із 60
✅ загальне середнє лежить у порожнечі між двома групами

3 · Чотири типи графіка на тих самих даних¶

Одні й ті самі 60 чисел, чотири виклики. Заразом це і є subplots — сітка 2×2, де axes виявляється звичайним двовимірним масивом numpy.

In [7]:
fig, axes = plt.subplots(2, 2, figsize=(12, 7), constrained_layout=True)

print("тип обʼєкта axes:", type(axes).__name__, "· форма:", axes.shape)

axes[0, 0].plot(дні + 1, виторг)
axes[0, 0].set_title("ax.plot — як змінювалось у часі")
axes[0, 0].set_xlabel("день роботи")
axes[0, 0].set_ylabel("виторг, грн")

axes[0, 1].scatter(дні + 1, виторг, s=18)
axes[0, 1].set_title("ax.scatter — ті самі значення без сполучень")
axes[0, 1].set_xlabel("день роботи")
axes[0, 1].set_ylabel("виторг, грн")

axes[1, 0].bar(дні + 1, виторг)
axes[1, 0].set_title("ax.bar — 60 категорій, яких ніхто не порівнює")
axes[1, 0].set_xlabel("день роботи")
axes[1, 0].set_ylabel("виторг, грн")

кількості, межі, _ = axes[1, 1].hist(виторг, bins=12)
axes[1, 1].set_title("ax.hist — як розподілені значення")
axes[1, 1].set_xlabel("виторг, грн")
axes[1, 1].set_ylabel("днів")

for ax in axes.flat:
    ax.grid(True, alpha=0.3)

plt.show()

print("скільки днів потрапило в кожну з 12 смуг:", кількості.astype(int))
тип обʼєкта axes: ndarray · форма: (2, 2)
No description has been provided for this image
скільки днів потрапило в кожну з 12 смуг: [ 8 10 17  9  0  0  0  0  3 10  1  2]
In [8]:
# гістограма показала два горби — перевіримо це числами, а не оком
порожніх_смуг = int((кількості == 0).sum())
лівий_горб = int(кількості[:6].sum())
правий_горб = int(кількості[6:].sum())

print("смуг усього        :", len(кількості))
print("порожніх смуг      :", порожніх_смуг)
print("днів у лівому горбі:", лівий_горб)
print("днів у правому     :", правий_горб)

assert порожніх_смуг == 4, "між горбами мала бути порожнеча"
assert лівий_горб == 44 and правий_горб == 16, "горби мали вийти 44 і 16 днів"
assert лівий_горб == (~вихідний).sum(), "лівий горб — це саме будні"
print("✅ два горби — це рівно будні (44 дні) і вихідні (16 днів)")
смуг усього        : 12
порожніх смуг      : 4
днів у лівому горбі: 44
днів у правому     : 16
✅ два горби — це рівно будні (44 дні) і вихідні (16 днів)

4 · Підписи, заголовок, легенда¶

Той самий графік двічі: без підписів і з підписами. Порівняй, скільки часу йде на розуміння кожного.

In [9]:
виторг_торік = виторг - 430          # умовний ряд для порівняння — та сама форма, нижчий рівень

fig, (ax_голий, ax_підписаний) = plt.subplots(1, 2, figsize=(13, 4.2),
                                              sharey=True, constrained_layout=True)

# ліворуч — рівно те, що виходить без жодного зайвого рядка
ax_голий.plot(дні + 1, виторг)
ax_голий.plot(дні + 1, виторг_торік)

# праворуч — ті самі два виклики плюс чотири рядки підписів
ax_підписаний.plot(дні + 1, виторг, label="2025")
ax_підписаний.plot(дні + 1, виторг_торік, label="2024")
ax_підписаний.set_xlabel("день роботи")
ax_підписаний.set_ylabel("виторг, грн")
ax_підписаний.set_title("Кавʼярня «Дві ложки»: щоденний виторг")
ax_підписаний.legend()
ax_підписаний.grid(True, alpha=0.3)

plt.show()

print("Ліворуч: дві лінії, невідомо чого і в чому. Праворуч: те саме плюс 4 рядки коду.")
print("Легенда бере назви саме з label= — без нього вона була б порожньою.")
No description has been provided for this image
Ліворуч: дві лінії, невідомо чого і в чому. Праворуч: те саме плюс 4 рядки коду.
Легенда бере назви саме з label= — без нього вона була б порожньою.

5 · Навіщо sharey=True¶

Два графіки поруч виглядають порівнюваними навіть тоді, коли шкали в них різні. Це одна з найтихіших помилок у звітах — і найлегша для виправлення.

In [10]:
перша_половина = виторг[:30]
друга_половина = виторг[30:]

fig, axes = plt.subplots(2, 2, figsize=(12, 6), constrained_layout=True)

# верхній ряд: кожен графік має власну шкалу — matplotlib підбирає її сам
axes[0, 0].plot(перша_половина)
axes[0, 0].set_title("дні 1-30 · власна шкала")
axes[0, 1].plot(друга_половина)
axes[0, 1].set_title("дні 31-60 · власна шкала")

# нижній ряд: спільна шкала, задана явно однаковими межами
спільні_межі = (виторг.min() - 100, виторг.max() + 100)
axes[1, 0].plot(перша_половина)
axes[1, 0].set_title("дні 1-30 · спільна шкала")
axes[1, 0].set_ylim(спільні_межі)
axes[1, 1].plot(друга_половина)
axes[1, 1].set_title("дні 31-60 · спільна шкала")
axes[1, 1].set_ylim(спільні_межі)

for ax in axes.flat:
    ax.set_ylabel("виторг, грн")
    ax.grid(True, alpha=0.3)

plt.show()

межі_зверху_зліва = axes[0, 0].get_ylim()
межі_зверху_справа = axes[0, 1].get_ylim()
print("верхній ряд, межі лівого графіка :", tuple(round(m) for m in межі_зверху_зліва))
print("верхній ряд, межі правого графіка:", tuple(round(m) for m in межі_зверху_справа))
assert межі_зверху_зліва != межі_зверху_справа, "шкали мали вийти різними"
print("✅ у верхньому ряду шкали різні — однакова висота піків означає РІЗНІ гривні")
print("   у нижньому ряду шкала одна, і видно, що друга половина справді вища")
No description has been provided for this image
верхній ряд, межі лівого графіка : (3597, 5163)
верхній ряд, межі правого графіка: (3765, 5551)
✅ у верхньому ряду шкали різні — однакова висота піків означає РІЗНІ гривні
   у нижньому ряду шкала одна, і видно, що друга половина справді вища

6 · Зберегти у файл: чому dpi має значення¶

Той самий аркуш зберігаємо двічі — з різною роздільністю — і дивимось на розміри в байтах. Файли кладемо у тимчасову теку, щоб нічого не лишилось на диску.

In [11]:
import tempfile
import pathlib

тека = pathlib.Path(tempfile.mkdtemp(prefix="dvi-lozhky-"))
print("тимчасова тека:", тека)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(дні + 1, виторг, label="2025")
ax.set_xlabel("день роботи")
ax.set_ylabel("виторг, грн")
ax.set_title("Кавʼярня «Дві ложки»: щоденний виторг")
ax.legend()
ax.grid(True, alpha=0.3)

# ЗБЕРІГАЄМО ДО show(): у багатьох середовищах show() звільняє аркуш
for роздільність in (72, 150, 300):
    ax.figure.savefig(тека / f"виторг-{роздільність}.png",
                      dpi=роздільність, bbox_inches="tight")

plt.show()

for файл in sorted(тека.glob("*.png")):
    розмір = файл.stat().st_size
    print(f"{файл.name:<18} {розмір:>8} байтів")
тимчасова тека: /tmp/dvi-lozhky-t6k7_7d4
No description has been provided for this image
виторг-150.png        78219 байтів
виторг-300.png       173168 байтів
виторг-72.png         33936 байтів
In [12]:
from PIL import Image        # іде в комплекті з matplotlib; потрібна лише щоб зміряти пікселі

for роздільність in (72, 150, 300):
    з_картинкою = Image.open(тека / f"виторг-{роздільність}.png")
    ширина, висота = з_картинкою.size
    очікувана_ширина = 8 * роздільність          # figsize=(8, 4) помножений на dpi
    print(f"dpi={роздільність:>3}: {ширина}×{висота} пікселів "
          f"(figsize×dpi дало б {очікувана_ширина}; bbox_inches='tight' зрізав поля)")
    # bbox_inches='tight' трохи підрізає поля, тому допускаємо відхилення
    assert abs(ширина - очікувана_ширина) < 0.2 * очікувана_ширина

малий = (тека / "виторг-72.png").stat().st_size
великий = (тека / "виторг-300.png").stat().st_size
print()
print(f"файл на 300 dpi важчий за файл на 72 dpi у {великий / малий:.1f} раза")
assert великий > малий, "більша роздільність має давати більший файл"
print("✅ розмір у пікселях = figsize × dpi, і платимо ми за це байтами")
dpi= 72: 510×285 пікселів (figsize×dpi дало б 576; bbox_inches='tight' зрізав поля)
dpi=150: 1065×594 пікселів (figsize×dpi дало б 1200; bbox_inches='tight' зрізав поля)
dpi=300: 2127×1190 пікселів (figsize×dpi дало б 2400; bbox_inches='tight' зрізав поля)

файл на 300 dpi важчий за файл на 72 dpi у 5.1 раза
✅ розмір у пікселях = figsize × dpi, і платимо ми за це байтами

7 · Те саме через df.plot()¶

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

In [13]:
продажі = pd.DataFrame({
    "день": дні + 1,
    "виторг": виторг,
    "вихідний": вихідний,
})

print(продажі.head())
print()

# найкоротший спосіб подивитись на дані під час роботи
осі_від_pandas = продажі.plot(x="день", y="виторг", figsize=(9, 3.6))
plt.show()

print("що повернув df.plot():", type(осі_від_pandas).__name__)
assert isinstance(осі_від_pandas, matplotlib.axes.Axes), "df.plot має повертати Axes"
print("✅ це той самий Axes — отже, усі методи ax.* до нього теж застосовні")
   день  виторг  вихідний
0     1    3691     False
1     2    3673     False
2     3    3956     False
3     4    3938     False
4     5    3860     False

No description has been provided for this image
що повернув df.plot(): Axes
✅ це той самий Axes — отже, усі методи ax.* до нього теж застосовні
In [14]:
# групування з теми 34 плюс графік: середній виторг окремо для буднів і вихідних
середні_по_групах = продажі.groupby("вихідний")["виторг"].mean().round(1)
середні_по_групах.index = ["будній день", "вихідний"]
print(середні_по_групах)
print()

fig, ax = plt.subplots(figsize=(9, 3.8))

# pandas малює в НАШУ область — тому далі ми можемо все доналаштувати звичайними методами
продажі.plot(x="день", y="виторг", ax=ax, label="щоденний виторг")
ax.axhline(середні_по_групах["будній день"], linestyle="--", linewidth=1.2,
           label=f"середнє по буднях ({середні_по_групах['будній день']:.0f} грн)")
ax.axhline(середні_по_групах["вихідний"], linestyle=":", linewidth=1.4,
           label=f"середнє по вихідних ({середні_по_групах['вихідний']:.0f} грн)")
ax.set_xlabel("день роботи")
ax.set_ylabel("виторг, грн")
ax.set_title("Дві прямі, між якими не лежить майже жоден день")
ax.legend(loc="upper left", fontsize=9)
ax.grid(True, alpha=0.3)
plt.show()

print("df.plot() — для розвідки; fig, ax — коли картинку хтось побачить.")
будній день    3984.8
вихідний       5124.4
Name: виторг, dtype: float64

No description has been provided for this image
df.plot() — для розвідки; fig, ax — коли картинку хтось побачить.

8 · Обрізана вісь: та сама різниця, два висновки¶

Середній чек кавʼярні по кварталах — 96, 98, 101 і 104 гривні. Намалюємо ті самі чотири числа двічі й порахуємо, у скільки разів змінюється враження.

In [15]:
квартали = ["Q1", "Q2", "Q3", "Q4"]
чек = np.array([96, 98, 101, 104], dtype=float)

справжнє_зростання = (чек[-1] - чек[0]) / чек[0] * 100
нижня_межа = 95
висота_Q1 = чек[0] - нижня_межа            # довжина стовпчика на обрізаному графіку
висота_Q4 = чек[-1] - нижня_межа
у_скільки_разів_вищий = висота_Q4 / висота_Q1

fig, (ax_обрізаний, ax_чесний) = plt.subplots(1, 2, figsize=(12, 4), constrained_layout=True)

ax_обрізаний.bar(квартали, чек)
ax_обрізаний.set_ylim(нижня_межа, 107)     # ось вона, єдина відмінність
ax_обрізаний.set_title(f"вісь від {нижня_межа} — «зростання вдевʼятеро»")
ax_обрізаний.set_ylabel("середній чек, грн")

ax_чесний.bar(квартали, чек)
ax_чесний.set_ylim(0, 115)                 # запас угорі, щоб підпис 104 не обрізався
ax_чесний.set_title("вісь від нуля — справжні +8.3 %")
ax_чесний.set_ylabel("середній чек, грн")

for ax in (ax_обрізаний, ax_чесний):
    for квартал, значення in zip(квартали, чек):
        ax.text(квартал, значення, f"{значення:.0f}", ha="center", va="bottom")

plt.show()

print(f"справжнє зростання за рік        : {справжнє_зростання:.1f} %")
print(f"довжини стовпчиків у одиницях осі при межі {нижня_межа}: "
      f"Q1 = {висота_Q1:.0f}, Q4 = {висота_Q4:.0f}")
print(f"на екрані Q4 вищий за Q1         : у {у_скільки_разів_вищий:.1f} раза")
print(f"перебільшення                    : у {(висота_Q4 / висота_Q1 - 1) * 100 / справжнє_зростання:.0f} разів")

assert round(справжнє_зростання, 1) == 8.3, "зростання мало вийти 8.3 %"
assert round(у_скільки_разів_вищий, 1) == 9.0, "на обрізаній осі Q4 мав стати вдевʼятеро вищим"
print("✅ ті самі чотири числа: +8.3 % у даних і «вдевʼятеро» на екрані")
No description has been provided for this image
справжнє зростання за рік        : 8.3 %
довжини стовпчиків у одиницях осі при межі 95: Q1 = 1, Q4 = 9
на екрані Q4 вищий за Q1         : у 9.0 раза
перебільшення                    : у 96 разів
✅ ті самі чотири числа: +8.3 % у даних і «вдевʼятеро» на екрані

9 · Прибираємо за собою¶

Тимчасова тека більше не потрібна — видаляємо її разом із картинками.

In [16]:
import shutil

файлів_було = len(list(тека.glob("*")))
shutil.rmtree(тека)

print("видалено файлів:", файлів_було)
print("тека існує   :", тека.exists())
assert not тека.exists(), "тимчасова тека мала зникнути"
print("✅ на диску нічого не лишилось")
видалено файлів: 3
тека існує   : False
✅ на диску нічого не лишилось

Завдання¶

🟢 Рівень 1 — База¶

Візьми масив виторг і побудуй один графік, у якого є все: заголовок, підпис осі x, підпис осі y з одиницями, легенда й напівпрозора сітка. Додай другою лінією виторг_торік — і збережи результат у файл із dpi=200. Зроблено, якщо: файл створився, ax.get_ylabel() повертає непорожній рядок, а len(ax.get_legend().get_texts()) == 2.

🟡 Рівень 2 — Плюс¶

Порахуй середній виторг по днях тижня (дні % 7) і покажи його стовпчиками, а поруч, у другій області тієї самої сітки, — гістограму всього виторгу. Зроблено, якщо: у стовпчиках видно, що два дні тижня помітно вищі за решту, і assert підтверджує, що це саме дні з індексами 5 і 6.

🔴 Рівень 3 — Виклик¶

Знайди третій спосіб збрехати графіком, якого не було в лекції (наприклад, подвійна вісь y з двома різними шкалами або пропущені дні на осі часу), намалюй пару «оманливо / чесно» й порахуй числом, у скільки разів відрізняється враження. Зроблено, якщо: обидва графіки побудовані з одного й того самого масиву даних, а різниця вражень підтверджена assert-ом на конкретне число.

Повні умови з критеріями — у homework.md.