Сделана более реалистичная генерация демо данных по людям
This commit is contained in:
@@ -7,3 +7,4 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
.venv/
|
.venv/
|
||||||
data/output/
|
data/output/
|
||||||
|
data/input/
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
DAG для демонстрации работы с файлами в Airflow
|
DAG для демонстрации работы с файлами в Airflow
|
||||||
Уровень: Средний
|
Уровень: Средний
|
||||||
|
|
||||||
|
Этот DAG генерирует русскоязычные данные с использованием библиотеки mimesis, включая:
|
||||||
|
- ФИО (с учетом пола)
|
||||||
|
- Возраст
|
||||||
|
- Зарплату
|
||||||
|
- Адрес проживания
|
||||||
"""
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import os
|
import os
|
||||||
@@ -24,7 +30,7 @@ dag = DAG(
|
|||||||
'file_operations_dag',
|
'file_operations_dag',
|
||||||
default_args=default_args,
|
default_args=default_args,
|
||||||
description='DAG для изучения работы с файлами в Airflow',
|
description='DAG для изучения работы с файлами в Airflow',
|
||||||
schedule_interval=timedelta(days=1),
|
schedule_interval=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
tags=['educational', 'files', 'intermediate']
|
tags=['educational', 'files', 'intermediate']
|
||||||
)
|
)
|
||||||
@@ -32,33 +38,56 @@ dag = DAG(
|
|||||||
def generate_sample_data():
|
def generate_sample_data():
|
||||||
"""Создание CSV файла с примерными данными"""
|
"""Создание CSV файла с примерными данными"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import random
|
from mimesis import Person, Address, Numeric
|
||||||
|
from mimesis.enums import Gender
|
||||||
|
from mimesis.locales import Locale
|
||||||
|
|
||||||
|
# Создаем генераторы с русской локализацией
|
||||||
|
person = Person(Locale.RU)
|
||||||
|
address = Address(Locale.RU)
|
||||||
|
|
||||||
|
# Генерируем данные
|
||||||
|
names = [person.full_name(gender=Gender.FEMALE if i % 3 == 0 else Gender.MALE) for i in range(1, 101)]
|
||||||
|
numeric = Numeric()
|
||||||
|
ages = [numeric.integer_number(start=18, end=65) for _ in range(100)] # Используем правильный метод для генерации возраста
|
||||||
|
occupations = [person.occupation() for _ in range(100)]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'id': range(1, 101),
|
'id': range(1, 101),
|
||||||
'name': [f'User_{i}' for i in range(1, 101)],
|
'name': names,
|
||||||
'age': [random.randint(18, 65) for _ in range(100)],
|
'age': ages,
|
||||||
'salary': [random.randint(30000, 100000) for _ in range(100)]
|
'salary': [max(30000, min(150000, ages[i] * 1200 + (hash(names[i]) % 15000) - 5000)) for i in range(100)],
|
||||||
|
'address': [address.address() for _ in range(100)]
|
||||||
}
|
}
|
||||||
|
|
||||||
df = pd.DataFrame(data)
|
df = pd.DataFrame(data)
|
||||||
df.to_csv('/opt/airflow/data/input/sample_data.csv', index=False)
|
df.to_csv('/opt/airflow/data/input/sample_data.csv', index=False)
|
||||||
print(f"Создан файл с {len(df)} записями")
|
print(f"Создан файл с {len(df)} записями")
|
||||||
|
print(f"Пример данных: {data['name'][0]}, возраст: {data['age'][0]}, зарплата: {data['salary'][0]}, адрес: {data['address'][0]}")
|
||||||
return "sample_data.csv created"
|
return "sample_data.csv created"
|
||||||
|
|
||||||
def read_and_validate_data():
|
def read_and_validate_data():
|
||||||
"""Чтение и валидация CSV файла"""
|
"""Чтение и валидация CSV файла"""
|
||||||
df = pd.read_csv('/opt/airflow/data/input/sample_data.csv')
|
df = pd.read_csv('/opt/airflow/data/input/sample_data.csv')
|
||||||
print(f"Прочитан файл: {len(df)} строк, {len(df.columns)} столбцов")
|
print(f"Прочитан файл: {len(df)} строк, {len(df.columns)} столбцов")
|
||||||
|
print(f"Колонки: {list(df.columns)}")
|
||||||
|
|
||||||
# Простая валидация
|
# Простая валидация
|
||||||
assert len(df) > 0, "Файл пустой"
|
assert len(df) > 0, "Файл пустой"
|
||||||
assert 'name' in df.columns, "Отсутствует столбец name"
|
assert 'name' in df.columns, "Отсутствует столбец name"
|
||||||
|
assert 'address' in df.columns, "Отсутствует столбец address"
|
||||||
|
|
||||||
|
# Дополнительная валидация для новых данных
|
||||||
|
assert df['name'].notna().all(), "Найдены пустые значения в столбце name"
|
||||||
|
assert df['address'].notna().all(), "Найдены пустые значения в столбце address"
|
||||||
|
assert df['age'].between(16, 100).all(), "Некорректные значения возраста"
|
||||||
|
assert df['salary'].between(20000, 150000).all(), "Некорректные значения зарплаты"
|
||||||
|
|
||||||
return f"Файл валидирован: {len(df)} записей"
|
return f"Файл валидирован: {len(df)} записей"
|
||||||
|
|
||||||
def transform_data():
|
def transform_data():
|
||||||
"""Преобразование данных"""
|
"""Преобразование данных"""
|
||||||
|
import os
|
||||||
df = pd.read_csv('/opt/airflow/data/input/sample_data.csv')
|
df = pd.read_csv('/opt/airflow/data/input/sample_data.csv')
|
||||||
|
|
||||||
# Простое преобразование - добавим столбец с категорией зарплаты
|
# Простое преобразование - добавим столбец с категорией зарплаты
|
||||||
@@ -66,9 +95,21 @@ def transform_data():
|
|||||||
lambda x: 'High' if x >= 70000 else 'Medium' if x >= 50000 else 'Low'
|
lambda x: 'High' if x >= 70000 else 'Medium' if x >= 50000 else 'Low'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Добавим дополнительные трансформации для новых данных
|
||||||
|
# Извлечем город из адреса (первое слово в адресе часто является городом)
|
||||||
|
df['city'] = df['address'].str.split().str[0]
|
||||||
|
# Преобразуем возраст в возрастную категорию
|
||||||
|
df['age_category'] = df['age'].apply(
|
||||||
|
lambda x: 'Young' if x < 30 else 'Middle-aged' if x < 50 else 'Senior'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем директорию, если она не существует
|
||||||
|
os.makedirs('/opt/airflow/data/output', exist_ok=True)
|
||||||
|
|
||||||
# Сохраняем обработанные данные
|
# Сохраняем обработанные данные
|
||||||
df.to_csv('/opt/airflow/data/output/processed_data.csv', index=False)
|
df.to_csv('/opt/airflow/data/output/processed_data.csv', index=False)
|
||||||
print(f"Обработаны данные: {len(df)} записей")
|
print(f"Обработаны данные: {len(df)} записей")
|
||||||
|
print(f"Добавлены новые столбцы: city, age_category")
|
||||||
|
|
||||||
return f"Данные обработаны: {len(df)} записей"
|
return f"Данные обработаны: {len(df)} записей"
|
||||||
|
|
||||||
@@ -79,9 +120,15 @@ def write_summary():
|
|||||||
summary = {
|
summary = {
|
||||||
'total_records': len(df),
|
'total_records': len(df),
|
||||||
'avg_salary': df['salary'].mean(),
|
'avg_salary': df['salary'].mean(),
|
||||||
|
'avg_age': df['age'].mean(),
|
||||||
'high_salary_count': len(df[df['salary_category'] == 'High']),
|
'high_salary_count': len(df[df['salary_category'] == 'High']),
|
||||||
'medium_salary_count': len(df[df['salary_category'] == 'Medium']),
|
'medium_salary_count': len(df[df['salary_category'] == 'Medium']),
|
||||||
'low_salary_count': len(df[df['salary_category'] == 'Low'])
|
'low_salary_count': len(df[df['salary_category'] == 'Low']),
|
||||||
|
'young_count': len(df[df['age_category'] == 'Young']),
|
||||||
|
'middle_aged_count': len(df[df['age_category'] == 'Middle-aged']),
|
||||||
|
'senior_count': len(df[df['age_category'] == 'Senior']),
|
||||||
|
'unique_cities': df['city'].nunique(),
|
||||||
|
'top_cities': df['city'].value_counts().head(5).to_dict()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Сохраняем сводку в текстовый файл
|
# Сохраняем сводку в текстовый файл
|
||||||
@@ -89,9 +136,15 @@ def write_summary():
|
|||||||
f.write("Сводка по обработанным данным:\n")
|
f.write("Сводка по обработанным данным:\n")
|
||||||
f.write(f"Всего записей: {summary['total_records']}\n")
|
f.write(f"Всего записей: {summary['total_records']}\n")
|
||||||
f.write(f"Средняя зарплата: {summary['avg_salary']:.2f}\n")
|
f.write(f"Средняя зарплата: {summary['avg_salary']:.2f}\n")
|
||||||
|
f.write(f"Средний возраст: {summary['avg_age']:.2f}\n")
|
||||||
f.write(f"Высокая зарплата: {summary['high_salary_count']}\n")
|
f.write(f"Высокая зарплата: {summary['high_salary_count']}\n")
|
||||||
f.write(f"Средняя зарплата: {summary['medium_salary_count']}\n")
|
f.write(f"Средняя зарплата: {summary['medium_salary_count']}\n")
|
||||||
f.write(f"Низкая зарплата: {summary['low_salary_count']}\n")
|
f.write(f"Низкая зарплата: {summary['low_salary_count']}\n")
|
||||||
|
f.write(f"Молодые (до 30): {summary['young_count']}\n")
|
||||||
|
f.write(f"Среднего возраста (30-49): {summary['middle_aged_count']}\n")
|
||||||
|
f.write(f"Пожилые (50+): {summary['senior_count']}\n")
|
||||||
|
f.write(f"Уникальных городов: {summary['unique_cities']}\n")
|
||||||
|
f.write(f"Топ-5 городов: {summary['top_cities']}\n")
|
||||||
|
|
||||||
print("Создана сводка по данным")
|
print("Создана сводка по данным")
|
||||||
return "Сводка создана"
|
return "Сводка создана"
|
||||||
|
|||||||
@@ -7,5 +7,8 @@ psycopg2-binary==2.9.9
|
|||||||
# Data processing (used in data_processing_dag.py and file_operations_dag.py)
|
# Data processing (used in data_processing_dag.py and file_operations_dag.py)
|
||||||
pandas==2.1.4
|
pandas==2.1.4
|
||||||
|
|
||||||
|
# Data generation library (used in file_operations_dag.py)
|
||||||
|
mimesis==15.1.0
|
||||||
|
|
||||||
# Airflow PostgreSQL provider (used in sql_basic_dag.py and data_processing_dag.py)
|
# Airflow PostgreSQL provider (used in sql_basic_dag.py and data_processing_dag.py)
|
||||||
apache-airflow-providers-postgres==5.11.1
|
apache-airflow-providers-postgres==5.11.1
|
||||||
|
|||||||
Reference in New Issue
Block a user