Добавить в корзинуПозвонить
Найти в Дзене

Прапор

Ниже представлен цикл статей, подробно описывающих каждый этап обработки и визуализации данных из представленного Jupyter Notebook. Статьи содержат объяснения, контекст, вставки кода и рекомендации. Они могут быть использованы как самостоятельная документация или расширенное руководство.
---
# Серия мини-статей: Обработка и визуализация региональных данных
## Введение

Ниже представлен цикл статей, подробно описывающих каждый этап обработки и визуализации данных из представленного Jupyter Notebook. Статьи содержат объяснения, контекст, вставки кода и рекомендации. Они могут быть использованы как самостоятельная документация или расширенное руководство.

---

# Серия мини-статей: Обработка и визуализация региональных данных

## Введение

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

В данном цикле статей мы разберём пошаговый подход, реализованный в Jupyter Notebook `module_A_B.ipynb`. Он включает:

- автоматическую загрузку CSV с определением кодировки и разделителя;

- глубокую диагностику качества данных;

- очистку и заполнение пропусков;

- объединение таблиц на уровне регионов;

- расчёт новых метрик (урожайность, плотность сетей, климатический индекс и др.);

- создание широкого спектра визуализаций: от статических графиков до интерактивных карт и анимаций.

Каждая статья посвящена одному логическому блоку и содержит пояснения, фрагменты кода и практические советы.

---

## Статья 1. Универсальная загрузка CSV и диагностика

### Зачем это нужно?

CSV-файлы могут иметь разные кодировки (UTF-8, CP1251, Latin-1), разделители (запятая, точка с запятой, табуляция), а также содержать «мусор» в именах столбцов (пробелы, BOM-символы). Без предварительной диагностики легко пропустить проблемы, которые приведут к ошибкам на этапе объединения или визуализации.

### Подход

Функция `load_csv_safe` перебирает несколько комбинаций кодировок и разделителей, пока не загрузит файл успешно (определяется по количеству столбцов > 1). Затем очищаются имена столбцов: удаляются пробелы, BOM-символы, всё приводится к нижнему регистру, пробелы заменяются подчёркиваниями.

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

### Код с пояснениями

```python

def load_csv_safe(path: str, name: str = None) -> pd.DataFrame:

"""

Загружает CSV с автоопределением разделителя и кодировки.

Возвращает DataFrame и печатает диагностику.

"""

name = name or os.path.basename(path)

encodings = ['utf-8', 'utf-8-sig', 'cp1251', 'latin-1']

separators = [',', ';', '\t', '|']

df = None

used_enc, used_sep = None, None

for enc in encodings:

for sep in separators:

try:

df = pd.read_csv(path, encoding=enc, sep=sep,

engine='python', on_bad_lines='warn')

if df.shape[1] > 1: # успех если более одного столбца

used_enc, used_sep = enc, sep

break

except Exception:

continue

if df is not None and df.shape[1] > 1:

break

if df is None or df.shape[1] <= 1:

print(f' ОШИБКА: не удалось загрузить {name}')

return pd.DataFrame()

# Очистка имён столбцов

df.columns = (df.columns.str.strip()

.str.replace('\ufeff', '', regex=False)

.str.lower()

.str.replace(' ', '_'))

# Печать диагностики

print(f'\n[{name}]')

print(f' Кодировка: {used_enc} | Разделитель: {repr(used_sep)}')

print(f' Строк: {len(df):,} | Столбцов: {df.shape[1]}')

print(f' Столбцы: {df.columns.tolist()}')

# Пропуски

miss = df.isnull().sum()

miss = miss[miss > 0]

if not miss.empty:

print(f' Пропуски:')

for col, cnt in miss.items():

print(f' {col}: {cnt} ({cnt/len(df)*100:.1f}%)')

else:

print(f' Пропусков нет')

# Дубликаты строк

dupes = df.duplicated().sum()

if dupes:

print(f' Дубликатов строк: {dupes}')

return df

```

### Дополнительная диагностика

Для более глубокого анализа используется функция `full_diagnostics`. Она выводит:

- типы столбцов;

- пропуски с процентами;

- количество дубликатов;

- выбросы (z-score > 3);

- описательную статистику для числовых столбцов;

- количество уникальных значений в категориальных столбцах.

Это позволяет быстро оценить качество данных и принять решение о дальнейших шагах (например, о необходимости удаления выбросов или выбора стратегии заполнения).

### Рекомендации

- Если файлы очень большие, стоит добавить параметр `nrows` для чтения только части строк во время экспериментов.

- Для ускорения можно использовать `engine='c'` (по умолчанию), но тогда обработка ошибок менее гибкая.

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

---

## Статья 2. Очистка и обработка пропусков

### Типичные проблемы CSV

Даже после успешной загрузки данные часто содержат:

- числа, записанные как строки с пробелами и запятыми (`"1 234,56"`);

- булевы значения в виде слов (`"да"/"нет"`, `"yes"/"no"`);

- даты в разных форматах;

- лишние пробелы в начале и конце строк;

- отрицательные значения там, где они не имеют смысла (например, площадь, население).

### Функция `fix_csv_issues`

Функция `fix_csv_issues` решает эти проблемы:

1. Удаляет пробелы вокруг строк.

2. Пытается преобразовать строки в числа, заменяя пробелы и запятые. Успешность проверяется по доле успешно сконвертированных значений (>80%).

3. Преобразует булевы строки в bool.

4. Если столбец содержит подстроку `date`, `год` и т.п., пытается преобразовать в datetime.

5. Для числовых столбцов с ключевыми словами (`population`, `area`, `harvest` и др.) заменяет отрицательные значения на абсолютные.

```python

def fix_csv_issues(df: pd.DataFrame, name: str = '') -> pd.DataFrame:

df = df.copy()

for col in df.columns:

if df[col].dtype == object:

# Убираем пробелы

df[col] = df[col].str.strip()

# Числа с пробелами и запятой

try:

cleaned = df[col].str.replace(' ', '', regex=False) \

.str.replace(',', '.', regex=False)

converted = pd.to_numeric(cleaned, errors='coerce')

if converted.notna().mean() > 0.8:

df[col] = converted

continue

except Exception:

pass

# Булевы значения

bool_map = {'да': True, 'нет': False, 'yes': True, 'no': False,

'1': True, '0': False, 'true': True, 'false': False}

lower_vals = df[col].str.lower().unique()

if all(v in bool_map for v in lower_vals if pd.notna(v)):

df[col] = df[col].str.lower().map(bool_map)

continue

# Даты

if any(kw in col for kw in ['date', 'дата', 'year', 'год']):

try:

df[col] = pd.to_datetime(df[col], errors='coerce')

except Exception:

pass

# Отрицательные значения

non_neg_keywords = ['population', 'area', 'harvest', 'урожай', 'население',

'electricity', 'потребление', 'длина', 'km', 'ha', 'tons']

for col in df.select_dtypes(include='number').columns:

if any(kw in col.lower() for kw in non_neg_keywords):

n_neg = (df[col] < 0).sum()

if n_neg:

df[col] = df[col].abs()

return df

```

### Заполнение пропусков

После очистки остаются пропуски. Функция `fill_missing` автоматически выбирает стратегию:

- для числовых столбцов — медиана (или среднее, zero, ffill по желанию);

- для категориальных — мода.

Можно передать пользовательский словарь стратегий для отдельных столбцов.

```python

def fill_missing(df: pd.DataFrame, strategy: dict = None) -> pd.DataFrame:

df = df.copy()

strategy = strategy or {}

for col in df.columns:

if df[col].isnull().sum() == 0:

continue

method = strategy.get(col, 'auto')

if method == 'drop':

df = df.dropna(subset=[col])

continue

if df[col].dtype in [np.float64, np.int64, 'float32', 'int32']:

if method in ('auto', 'median'):

val = df[col].median()

elif method == 'mean':

val = df[col].mean()

elif method == 'zero':

val = 0

elif method == 'ffill':

df[col] = df[col].ffill()

continue

df[col] = df[col].fillna(val)

else:

if method in ('auto', 'mode'):

val = df[col].mode()[0] if not df[col].mode().empty else 'Unknown'

df[col] = df[col].fillna(val)

return df

```

### Важные замечания

- Замена отрицательных значений на `abs()` может быть неверна для показателей, где отрицательные значения имеют смысл (например, температура). Лучше сузить список ключевых слов или добавить проверку по контексту.

- При использовании `ffill` для временных рядов убедитесь, что данные отсортированы.

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

---

## Статья 3. Объединение таблиц на уровне регионов

### Цель

Исходные данные имеют разную детализацию: некоторые таблицы содержат годовые наблюдения (электроэнергия), другие — данные по месяцам (климат), третьи — справочники (регионы). Нам необходимо свести всё к единой таблице, где каждая строка соответствует региону.

### Агрегация перед объединением

Прежде чем выполнять `join`, мы агрегируем данные до уровня региона:

- **Электроэнергия**: берём последний год (или можно сумму, среднее — по задаче).

- **Урожай**: суммарный сбор по всем культурам за последний год.

- **Климат**: годовые средние (температура, влажность), сумма осадков, минимумы и максимумы.

- **Города**: суммарное население и количество городов в регионе.

```python

# Электроэнергия: последний год

elec_last = (dfs['electricity']

.sort_values('year')

.groupby('region_id')

.last()

.reset_index()

.rename(columns={'electricity_gwh': 'elec_last_gwh', 'year': 'elec_year'}))

# Урожай: суммарный

harvest_total = (dfs['harvest']

.groupby('region_id')

.agg(harvest_total_tons=('harvest_tons', 'sum'),

sowing_total_ha=('sowing_ha', 'sum'))

.reset_index())

# Климат: средние за год

climate_avg = (dfs['climate']

.groupby('region_id')

.agg(temp_avg=('temp_c', 'mean'),

temp_min=('temp_c', 'min'),

temp_max=('temp_c', 'max'),

precip_annual_mm=('precip_mm', 'sum'),

humidity_avg=('humidity_pct', 'mean'))

.reset_index())

# Население городов

city_pop = (dfs['cities']

.groupby('region_id')

.agg(total_city_pop=('population', 'sum'),

n_cities=('city_id', 'count'))

.reset_index())

```

### Последовательное слияние

Базовой таблицей служит `regions`. К ней последовательно присоединяются остальные агрегированные данные через `left join` по ключу `region_id`. При слиянии автоматически удаляются дублирующиеся столбцы (с суффиксом `_dup`), которые могли бы появиться из-за одинаковых имён в разных таблицах.

```python

base = dfs['regions'].copy()

df_master = base.copy()

joins = [

(dfs['density'], 'region_id', 'left'),

(dfs['terrain'], 'region_id', 'left'),

(dfs['grid'], 'region_id', 'left'),

(dfs['economy'], 'region_id', 'left'),

(elec_last, 'region_id', 'left'),

(harvest_total, 'region_id', 'left'),

(climate_avg, 'region_id', 'left'),

(city_pop, 'region_id', 'left'),

]

for right, key, how in joins:

df_master = df_master.merge(right, on=key, how=how, suffixes=('', '_dup'))

dup_cols = [c for c in df_master.columns if c.endswith('_dup')]

df_master.drop(columns=dup_cols, inplace=True)

```

### Возможные сложности

- Если в какой-то таблице отсутствуют данные для некоторых регионов, после `left join` появятся `NaN`. Это нормально, но позже нужно будет решить, как их обработать.

- При слиянии нескольких таблиц важно следить за порядком, чтобы не потерять информацию.

- При использовании `suffixes=('', '_dup')` мы предполагаем, что столбцы в базовой таблице не имеют суффикса, а все конфликтующие будут помечены `_dup` и удалены.

---

## Статья 4. Расчёт производных показателей

### Зачем добавлять новые метрики?

Исходные данные часто не содержат готовых аналитических показателей. Например, урожайность (т/га) требует деления сбора на площадь посева, а плотность сетей — деления длины проводов на площадь региона. Такие показатели делают анализ более информативным и позволяют строить содержательные визуализации.

### Добавленные показатели

1. **Доля посевных площадей** (`sowing_share`) — процент площади региона, занятой под посевы.

`area_ha = area_km2 * 100`;

`sowing_share = sowing_total_ha / area_ha * 100`.

2. **Урожайность** (`yield_t_per_ha`) — сбор урожая (тонн) на гектар посевов.

`yield_t_per_ha = harvest_total_tons / sowing_total_ha`.

3. **Потребление электроэнергии на душу** (`elec_per_capita`) — в кВт·ч на человека.

`elec_per_capita = elec_last_gwh * 1e6 / total_city_pop`.

4. **Плотность электросетей** (`grid_density`) — километров проводов на квадратный километр площади.

`grid_density = grid_km / area_km2`.

5. **Климатический индекс Де Мартонна** (`climate_index`) — отношение годовой суммы осадков к (средняя температура + 10).

Значения > 20 указывают на влажный климат, < 10 — на засушливый.

```python

if 'area_km2' in df_master.columns and 'sowing_total_ha' in df_master.columns:

df_master['area_ha'] = df_master['area_km2'] * 100

df_master['sowing_share'] = (df_master['sowing_total_ha'] / df_master['area_ha'] * 100).clip(0, 100)

if 'harvest_total_tons' in df_master.columns and 'sowing_total_ha' in df_master.columns:

df_master['yield_t_per_ha'] = (df_master['harvest_total_tons'] / df_master['sowing_total_ha'].replace(0, np.nan)).round(2)

if 'elec_last_gwh' in df_master.columns and 'total_city_pop' in df_master.columns:

df_master['elec_per_capita'] = (df_master['elec_last_gwh'] * 1e6 / df_master['total_city_pop'].replace(0, np.nan)).round(1)

if 'grid_km' in df_master.columns and 'area_km2' in df_master.columns:

df_master['grid_density'] = (df_master['grid_km'] / df_master['area_km2']).round(4)

if 'temp_avg' in df_master.columns and 'precip_annual_mm' in df_master.columns:

df_master['climate_index'] = (df_master['precip_annual_mm'] / (df_master['temp_avg'] + 10).replace(0, np.nan)).round(2)

```

### Композитный индекс

Чтобы сравнить регионы по нескольким критериям, можно создать интегральный показатель. В примере используется нормализация (MinMaxScaler) по выбранным метрикам и вычисление среднего значения.

```python

from sklearn.preprocessing import MinMaxScaler

score_cols = [c for c in ['sowing_share','yield_t_per_ha','climate_index']

if c in df_master.columns]

if score_cols:

scaler = MinMaxScaler()

df_master['composite_score'] = scaler.fit_transform(df_master[score_cols].fillna(0)).mean(axis=1)

```

### Примечания

- При делении на ноль или отсутствие данных используем `replace(0, np.nan)`, чтобы избежать деления на ноль и появления `inf`.

- Нормализация чувствительна к выбросам; если они есть, лучше использовать робастные методы (например, `RobustScaler`).

- Выбор весов для композитного индекса — задача, требующая экспертной оценки.

---

## Статья 5. Статистическая визуализация (matplotlib)

### Обзор

Первый блок визуализации создаёт набор классических статистических графиков с помощью `matplotlib`. Они помогают быстро оценить распределения, связи между переменными и временные тренды.

### Код с пояснениями

```python

plt.style.use('seaborn-v0_8-darkgrid')

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

fig.suptitle('Анализ региональных показателей', fontsize=16, fontweight='bold')

```

#### 1. Гистограмма населения

Показывает распределение регионов по численности населения. Добавлена вертикальная линия медианы.

```python

ax1 = axes[0, 0]

ax1.hist(df_master['total_city_pop'].dropna() / 1e6, bins=15, edgecolor='black', alpha=0.7, color='steelblue')

ax1.axvline(df_master['total_city_pop'].median() / 1e6, color='red', linestyle='--', label='Медиана')

ax1.set_xlabel('Население (млн чел)')

ax1.set_ylabel('Количество регионов')

ax1.set_title('Распределение населения по регионам')

ax1.legend()

```

#### 2. Диаграмма рассеяния: урожайность vs осадки

Цвет точек соответствует средней температуре, что позволяет увидеть взаимодействие трёх переменных.

```python

ax2 = axes[0, 1]

scatter = ax2.scatter(df_master['precip_annual_mm'], df_master['yield_t_per_ha'],

c=df_master['temp_avg'], cmap='coolwarm', s=100, alpha=0.6, edgecolors='black')

plt.colorbar(scatter, ax=ax2, label='Температура (°C)')

ax2.set_xlabel('Осадки (мм/год)')

ax2.set_ylabel('Урожайность (т/га)')

ax2.set_title('Урожайность vs Осадки (цвет = температура)')

```

#### 3. Box plot: влияние рельефа на урожайность

Сравнивает урожайность в регионах с горами и без гор.

```python

ax3 = axes[0, 2]

data_to_plot = [df_master[df_master['has_mountains'] == 0]['yield_t_per_ha'].dropna(),

df_master[df_master['has_mountains'] == 1]['yield_t_per_ha'].dropna()]

bp = ax3.boxplot(data_to_plot, labels=['Равнины', 'Горы'], patch_artist=True)

bp['boxes'][0].set_facecolor('lightgreen')

bp['boxes'][1].set_facecolor('lightcoral')

ax3.set_ylabel('Урожайность (т/га)')

ax3.set_title('Влияние рельефа на урожайность')

```

#### 4. Тепловая карта корреляций

Визуализирует матрицу корреляций между ключевыми числовыми показателями.

```python

ax4 = axes[1, 0]

num_cols_corr = ['total_city_pop', 'area_km2', 'gdp_mln', 'elec_last_gwh', 'yield_t_per_ha', 'temp_avg', 'precip_annual_mm']

available_cols = [c for c in num_cols_corr if c in df_master.columns]

corr_matrix = df_master[available_cols].corr()

im = ax4.imshow(corr_matrix, cmap='RdBu_r', vmin=-1, vmax=1)

ax4.set_xticks(range(len(available_cols)))

ax4.set_yticks(range(len(available_cols)))

ax4.set_xticklabels(available_cols, rotation=45, ha='right')

ax4.set_yticklabels(available_cols)

plt.colorbar(im, ax=ax4, label='Корреляция')

ax4.set_title('Матрица корреляций')

```

#### 5. Линейный график динамики электропотребления

Суммарное потребление электроэнергии по годам.

```python

ax5 = axes[1, 1]

elec_yearly = dfs['electricity'].groupby('year')['electricity_gwh'].sum()

ax5.plot(elec_yearly.index, elec_yearly.values, 'o-', linewidth=2, markersize=8, color='darkorange')

ax5.fill_between(elec_yearly.index, elec_yearly.values, alpha=0.3)

ax5.set_xlabel('Год')

ax5.set_ylabel('Потребление электроэнергии (GWh)')

ax5.set_title('Динамика потребления электроэнергии')

```

#### 6. Круговая диаграмма структуры урожая

```python

ax6 = axes[1, 2]

harvest_by_crop = dfs['harvest'].groupby('crop')['harvest_tons'].sum()

colors = plt.cm.Set3(range(len(harvest_by_crop)))

ax6.pie(harvest_by_crop.values, labels=harvest_by_crop.index, autopct='%1.1f%%', colors=colors, startangle=90)

ax6.set_title('Структура урожая по культурам')

```

### Сохранение

Все графики сохраняются в файл `statistical_analysis.png` в выходной директории.

---

## Статья 6. Интерактивные карты (Folium)

### Преимущества интерактивных карт

Статические карты хороши для отчётов, но интерактивные позволяют пользователю масштабировать, кликать по объектам, переключать слои. Folium — библиотека, создающая карты на основе Leaflet.js, которая легко интегрируется с Python.

### Создание базовой карты с маркерами

Функция `create_interactive_map` создаёт карту с центром, вычисленным по средним координатам точек. Добавляются два слоя подложки (OpenStreetMap, Stamen Terrain, Stamen Toner).

```python

def create_interactive_map(df, lat_col='lat', lon_col='lon',

popup_cols=['region_name', 'total_city_pop', 'yield_t_per_ha'],

size_col='total_city_pop', color_col='yield_t_per_ha'):

center_lat = df[lat_col].mean()

center_lon = df[lon_col].mean()

m = folium.Map(location=[center_lat, center_lon], zoom_start=5, tiles='OpenStreetMap')

folium.TileLayer('Stamen Terrain').add_to(m)

folium.TileLayer('Stamen Toner').add_to(m)

```

### Маркеры с переменным размером и цветом

Размер маркера пропорционален выбранному показателю (например, населению), цвет — другому показателю (урожайность). Для цветов используется нормализация и colormap из matplotlib.

```python

if size_col in df.columns:

size_norm = (df[size_col] - df[size_col].min()) / (df[size_col].max() - df[size_col].min())

sizes = size_norm * 30 + 10

if color_col in df.columns:

color_norm = (df[color_col] - df[color_col].min()) / (df[color_col].max() - df[color_col].min())

colors = [plt.cm.RdYlGn_r(x) for x in color_norm]

colors_hex = ['#{:02x}{:02x}{:02x}'.format(int(r*255), int(g*255), int(b*255)) for r, g, b, _ in colors]

```

Для каждой строки добавляется `CircleMarker` с всплывающим окном (popup), содержащим значения выбранных полей.

### Тепловая карта плотности населения

Folium позволяет добавить слой тепловой карты, где интенсивность зависит от значения показателя (например, численности населения). Это помогает визуализировать пространственное распределение.

```python

if 'total_city_pop' in df.columns:

heat_data = [[row[lat_col], row[lon_col], row['total_city_pop']] for idx, row in df.iterrows()]

HeatMap(heat_data, radius=15, blur=10, max_zoom=1).add_to(m)

```

### Сохранение и отображение

Карта сохраняется в HTML-файл и может быть открыта в браузере.

```python

map_path = os.path.join(OUTPUT_DIR, 'interactive_map.html')

m.save(map_path)

return m

```

В ноутбуке создаются две карты: для регионов и для городов.

---

## Статья 7. Динамические графики и анимации (Plotly)

### Почему Plotly?

Plotly позволяет создавать интерактивные графики, которые можно масштабировать, вращать (в 3D), а также анимировать по времени. Результаты легко сохраняются в HTML и встраиваются в веб-страницы.

### Анимированный временной ряд

График изменения потребления электроэнергии по регионам с возможностью просмотра динамики. В ноутбуке используется `px.line` с параметром `animation_frame`, но в представленном коде реализован более простой вариант — статический график с цветом по регионам. Однако для анимации используется отдельный блок `create_timeseries_animation` на основе matplotlib. Для полноты картины приведём пример с plotly:

```python

fig = px.line(dfs['electricity'], x='year', y='electricity_gwh', color='region_id',

title='Динамика потребления электроэнергии по регионам')

fig.update_layout(hovermode='x unified')

fig.show()

fig.write_html('dynamic_electricity.html')

```

### Анимированная пузырьковая диаграмма

Демонстрирует изменение ВРП и населения с течением времени. Для этого создаётся расширенный DataFrame с фиктивным годом (2020–2024) и случайным трендом.

```python

df_animated = df_master.copy()

years = [2020, 2021, 2022, 2023, 2024]

dfs_animated = []

for year in years:

temp_df = df_master.copy()

temp_df['year'] = year

temp_df['gdp_mln'] = temp_df['gdp_mln'] * (1 + (year - 2020) * 0.05) # тренд

dfs_animated.append(temp_df)

df_animated = pd.concat(dfs_animated, ignore_index=True)

fig = px.scatter(df_animated, x='total_city_pop', y='gdp_mln',

size='yield_t_per_ha', color='region_name',

animation_frame='year', animation_group='region_name',

hover_name='region_name', size_max=50,

title='Динамика экономических показателей по годам')

fig.update_layout(xaxis_type="log", yaxis_type="log")

fig.show()

```

### 3D-поверхность климатических данных

Визуализирует среднемесячные температуры по регионам. Используется `go.Surface` из `plotly.graph_objects`.

```python

climate_pivot = dfs['climate'].pivot_table(index='month', columns='region_id', values='temp_c', aggfunc='mean')

fig = go.Figure(data=[go.Surface(z=climate_pivot.values,

x=climate_pivot.columns,

y=climate_pivot.index,

colorscale='RdBu')])

fig.update_layout(title='Климатические профили регионов',

scene=dict(xaxis_title='Регион', yaxis_title='Месяц', zaxis_title='Температура (°C)'))

fig.show()

```

### Субплоты с разными типами графиков

`make_subplots` позволяет комбинировать различные типы графиков в одном окне. В примере собраны барчарты, scatter, pie, что даёт комплексное представление о сельском хозяйстве.

---

## Статья 8. Дашборд ключевых метрик

### Концепция дашборда

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

### Компоненты дашборда

1. **KPI-карточки** — показывают суммарное или среднее значение важных показателей (население, ВРП, урожайность, электропотребление).

Для каждой карточки создаётся отдельная ось, где текст центрируется.

```python

ax = fig.add_subplot(gs[0, i])

value = df[col].sum() / scale if 'total' in col else df[col].mean()

ax.text(0.5, 0.6, f'{value:,.0f}', ha='center', va='center', fontsize=24, fontweight='bold', color='darkblue')

ax.text(0.5, 0.3, f'{name}\\n{unit}', ha='center', va='center', fontsize=12, color='gray')

ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis('off'); ax.set_facecolor('#f0f0f0')

```

2. **Горизонтальная гистограмма** — топ-10 регионов по ВРП.

```python

ax = fig.add_subplot(gs[1, 0:2])

top10 = df.nlargest(10, 'gdp_mln')[['region_name', 'gdp_mln']]

colors = plt.cm.YlOrRd(np.linspace(0.3, 0.9, 10))

ax.barh(range(len(top10)), top10['gdp_mln'].values / 1000, color=colors[::-1])

ax.set_yticks(range(len(top10)))

ax.set_yticklabels(top10['region_name'].values)

ax.set_xlabel('ВРП (млрд руб)')

ax.set_title('Топ-10 регионов по ВРП', fontweight='bold')

```

3. **Радарная диаграмма** — профиль выбранного региона (первого в списке) по нормализованным показателям.

```python

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

radar_metrics = ['gdp_mln', 'total_city_pop', 'elec_last_gwh', 'yield_t_per_ha', 'grid_km']

radar_metrics = [m for m in radar_metrics if m in df.columns]

if radar_metrics:

values = [selected_region[m] / df[m].max() for m in radar_metrics]

angles = np.linspace(0, 2*np.pi, len(radar_metrics), endpoint=False).tolist()

values += values[:1]

angles += angles[:1]

ax.plot(angles, values, 'o-', linewidth=2, color='darkorange')

ax.fill(angles, values, alpha=0.25, color='orange')

ax.set_xticks(angles[:-1])

ax.set_xticklabels(radar_metrics, size=8)

ax.set_title(f'Профиль региона: {selected_region["region_name"]}', fontweight='bold', size=10)

```

4. **Тепловая карта корреляций** (упрощённая) — визуализация корреляций между выбранными числовыми столбцами.

```python

ax = fig.add_subplot(gs[2, :])

num_cols = ['total_city_pop', 'area_km2', 'gdp_mln', 'elec_last_gwh', 'yield_t_per_ha', 'grid_density']

num_cols = [c for c in num_cols if c in df.columns]

corr = df[num_cols].corr()

im = ax.imshow(corr, cmap='coolwarm', vmin=-1, vmax=1)

ax.set_xticks(range(len(num_cols))); ax.set_yticks(range(len(num_cols)))

ax.set_xticklabels(num_cols, rotation=45, ha='right')

ax.set_yticklabels(num_cols)

plt.colorbar(im, ax=ax, label='Корреляция', shrink=0.8)

ax.set_title('Матрица корреляций', fontweight='bold')

```

### Итоговое сохранение

Весь дашборд сохраняется как `dashboard.png` в выходной директории.

---

## Заключение

Цикл статей охватил все ключевые этапы работы с региональными данными: от загрузки «сырых» CSV до создания интерактивных дашбордов. Каждый этап снабжён пояснениями, кодом и рекомендациями, что позволяет использовать этот материал как готовое руководство для аналогичных проектов.

Основные выводы:

- Универсальная загрузка и диагностика данных — залог успешного дальнейшего анализа.

- Очистка и заполн

ение пропусков должны выполняться с учётом специфики каждого столбца.

- Объединение таблиц требует предварительной агрегации и выбора правильной стратегии join.

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

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

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