ETL Pipeline for Construction Data
Overview
Based on DDC methodology (Chapter 4.2), this skill enables building automated data pipelines that extract information from various sources, transform it into useful formats, and load it into target systems or generate reports.
Book Reference: "ETL и автоматизация процессов" / "ETL and Process Automation"
"ETL: переход от ручного управления к автоматизации позволяет компаниям обрабатывать данные без постоянного человеческого вмешательства." — DDC Book, Chapter 4.2
ETL Components
┌─────────┐ ┌───────────┐ ┌────────┐
│ EXTRACT │ -> │ TRANSFORM │ -> │ LOAD │
└─────────┘ └───────────┘ └────────┘
│ │ │
▼ ▼ ▼
Sources Process Outputs
- PDF - Clean - Excel
- Excel - Validate - PDF
- CSV - Calculate - Database
- BIM - Merge - API
- API - Aggregate - Dashboard
Quick Start
import pandas as pd
# Simple ETL Pipeline
def simple_etl_pipeline(input_file, output_file):
# EXTRACT
df = pd.read_excel(input_file)
# TRANSFORM
df = df.dropna() # Clean
df['Total'] = df['Quantity'] * df['Unit_Price'] # Calculate
summary = df.groupby('Category')['Total'].sum() # Aggregate
# LOAD
summary.to_excel(output_file)
return summary
# Run
result = simple_etl_pipeline("raw_data.xlsx", "processed_report.xlsx")
Extract: Data Sources
From Multiple Excel Files
import pandas as pd
from pathlib import Path
def extract_excel_files(folder_path, pattern="*.xlsx"):
"""Extract data from multiple Excel files"""
files = Path(folder_path).glob(pattern)
all_data = []
for file in files:
try:
df = pd.read_excel(file)
df['_source_file'] = file.name
all_data.append(df)
print(f"Extracted: {file.name}")
except Exception as e:
print(f"Error reading {file.name}: {e}")
if all_data:
return pd.concat(all_data, ignore_index=True)
return pd.DataFrame()
# Usage
df = extract_excel_files("./project_data/")
From PDF Documents
import pdfplumber
import pandas as pd
def extract_from_pdfs(pdf_folder):
"""Extract tables from all PDFs in folder"""
files = Path(pdf_folder).glob("*.pdf")
all_tables = []
for pdf_path in files:
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
tables = page.extract_tables()
for table in tables:
if table and len(table) > 1:
df = pd.DataFrame(table[1:], columns=table[0])
df['_source'] = pdf_path.name
all_tables.append(df)
return pd.concat(all_tables, ignore_index=True) if all_tables else pd.DataFrame()
From API
import requests
import pandas as pd
def extract_from_api(api_url, headers=None):
"""Extract data from REST API"""
response = requests.get(api_url, headers=headers)
if response.status_code == 200:
data = response.json()
return pd.DataFrame(data)
else:
raise Exception(f"API error: {response.status_code}")
# Usage
df = extract_from_api("https://api.example.com/projects")
From Database
import pandas as pd
import sqlite3
def extract_from_database(db_path, query):
"""Extract data using SQL query"""
conn = sqlite3.connect(db_path)
df = pd.read_sql_query(query, conn)
conn.close()
return df
# Usage
df = extract_from_database(
"construction.db",
"SELECT * FROM elements WHERE category = 'Wall'"
)
Transform: Data Processing
Data Cleaning
def clean_construction_data(df):
"""Standard cleaning for construction data"""
# Remove empty rows
df = df.dropna(how='all')
# Strip whitespace
for col in df.select_dtypes(include=['object']).columns:
df[col] = df[col].str.strip()
# Standardize category names
if 'Category' in df.columns:
df['Category'] = df['Category'].str.title()
# Convert numeric columns
numeric_cols = ['Volume', 'Area', 'Length', 'Quantity', 'Cost']
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce')
# Remove duplicates
df = df.drop_duplicates()
return df
Data Validation
def validate_construction_data(df, rules):
"""
Validate data against rules
Args:
rules: list of dicts like
[{'column': 'Volume', 'rule': 'positive'},
{'column': 'Category', 'rule': 'not_null'}]
"""
errors = []
for rule in rules:
col = rule['column']
rule_type = rule['rule']
if col not in df.columns:
errors.append(f"Missing column: {col}")
continue
if rule_type == 'positive':
invalid = df[df[col] <= 0]
if len(invalid) > 0:
errors.append(f"{len(invalid)} rows with non-positive {col}")
elif rule_type == 'not_null':
null_count = df[col].isna().sum()
if null_count > 0:
errors.append(f"{null_count} null values in {col}")
elif rule_type == 'unique':
duplicates = df[col].duplicated().sum()
if duplicates > 0:
errors.append(f"{duplicates} duplicate values in {col}")
return errors
# Usage
validation_rules = [
{'column': 'Volume', 'rule': 'positive'},
{'column': 'Category', 'rule': 'not_null'},
{'column': 'ElementId', 'rule': 'unique'}
]
errors = validate_construction_data(df, validation_rules)
Data Aggregation
def aggregate_by_hierarchy(df, hierarchy=['Project', 'Building', 'Level', 'Category']):
"""Aggregate data at different hierarchy levels"""
results = {}
for i in range(1, len(hierarchy) + 1):
level_cols = hierarchy[:i]
if all(col in df.columns for col in level_cols):
agg = df.groupby(level_cols).agg({
'Volume': 'sum',
'Cost': 'sum',
'ElementId': 'count'
}).rename(columns={'ElementId': 'Count'})
level_name = '_'.join(level_cols)
results[level_name] = agg
return results
# Usage
aggregations = aggregate_by_hierarchy(df)
for name, data in aggregations.items():
print(f"\n{name}:")
print(data.head())
Data Enrichment
def enrich_with_prices(df, prices_df):
"""Enrich element data with pricing information"""
# Merge with price database
enriched = df.merge(prices_df, on='Category', how='left')
# Calculate costs
enriched['Material_Cost'] = enriched['Volume'] * enriched['Unit_Price']
enriched['Labor_Cost'] = enriched['Volume'] * enriched['Labor_Rate']
enriched['Total_Cost'] = enriched['Material_Cost'] + enriched['Labor_Cost']
return enriched
Load: Output Generation
Generate Excel Report
def generate_excel_report(df, summary, output_path):
"""Generate formatted Excel report"""
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
# Raw data
df.to_excel(writer, sheet_name='Data', index=False)
# Summary by category
summary.to_excel(writer, sheet_name='Summary')
# Pivot table
if 'Level' in df.columns and 'Category' in df.columns:
pivot = pd.pivot_table(
df, values='Volume',
index='Level', columns='Category',
aggfunc='sum', fill_value=0
)
pivot.to_excel(writer, sheet_name='By_Level')
print(f"Report saved: {output_path}")
# Usage
generate_excel_report(df, summary, "project_report.xlsx")
Generate PDF Report
from reportlab.lib import colors
from reportlab.lib.pagesizes import l