Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Convert construction data to/from Parquet format. Optimize storage, enable fast queries, and integrate with data lakehouses.
.claude/skills/datadrivenconstruction-parquet-converter/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 282% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 37% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 256% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 152% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 138% | 0% |
Data storage and processing challenges:
Convert construction data to Parquet format for efficient columnar storage, faster queries, and compatibility with data lakehouses.
pythonimport pandas as pd from typing import Dict, Any, List, Optional, Union from dataclasses import dataclass, field from datetime import datetime from pathlib import Path import json class CompressionType: SNAPPY = "snappy" GZIP = "gzip" BROTLI = "brotli" ZSTD = "zstd" NONE = None @dataclass class ParquetSchema: columns: Dict[str, str] # column_name: dtype partitions: List[str] = field(default_factory=list) row_group_size: int = 100000 @dataclass class ConversionResult: source_path: str output_path: str source_format: str rows: int columns: int original_size_mb: float parquet_size_mb: float compression_ratio: float duration_seconds: float class ParquetConverter: """Convert construction data to/from Parquet format.""" def __init__(self, project_name: str = "Data Conversion"): self.project_name = project_name self.conversions: List[ConversionResult] = [] self.schemas: Dict[str, ParquetSchema] = {} self._define_standard_schemas() def _define_standard_schemas(self): """Define standard schemas for construction data.""" self.schemas['projects'] = ParquetSchema( columns={ 'project_id': 'string', 'name': 'string', 'project_type': 'category', 'status': 'category', 'start_date': 'datetime64[ns]', 'end_date': 'datetime64[ns]', 'budget': 'float64', 'actual_cost': 'float64', 'size_sf': 'float64', 'location': 'string' }, partitions=['project_type', 'status'] ) self.schemas['costs'] = ParquetSchema( columns={ 'transaction_id': 'string', 'project_id': 'string', 'cost_code': 'category', 'description': 'string', 'amount': 'float64', 'transaction_date': 'datetime64[ns]', 'vendor': 'string', 'invoice_number': 'string' }, partitions=['project_id'] ) self.schemas['schedule'] = ParquetSchema( columns={ 'activity_id': 'string', 'project_id': 'string', 'name': 'string', 'wbs_code': 'string', 'start_date': 'datetime64[ns]', 'end_date': 'datetime64[ns]', 'duration': 'int32', 'progress': 'float32', 'status': 'category' }, partitions=['project_id'] ) self.schemas['qto'] = ParquetSchema( columns={ 'element_id': 'string', 'project_id': 'string', 'element_type': 'category', 'name': 'string', 'quantity': 'float64', 'unit': 'category', 'level': 'string', 'material': 'string' }, partitions=['project_id', 'element_type'] ) def add_schema(self, name: str, schema: ParquetSchema): """Add custom schema.""" self.schemas[name] = schema def csv_to_parquet(self, csv_path: str, parquet_path: str, schema_name: str = None, compression: str = CompressionType.SNAPPY, partition_cols: List[str] = None) -> ConversionResult: """Convert CSV to Parquet.""" start_time = datetime.now() # Read CSV df = pd.read_csv(csv_path) # Apply schema if provided if schema_name and schema_name in self.schemas: schema = self.schemas[schema_name] df = self._apply_schema(df, schema) partition_cols = partition_cols or schema.partitions # Get original file size original_size = Path(csv_path).stat().st_size / (1024 * 1024) # Write Parquet if partition_cols: # Partitioned write available_partitions = [c for c in partition_cols if c in df.columns] if available_partitions: df.to_parquet( parquet_path, engine='pyarrow', compression=compression, partition_cols=available_partitions, index=False ) else: df.to_parquet(parquet_path, engine='pyarrow', compression=compression, index=False) else: df.to_parquet(parquet_path, engine='pyarrow', compression=compression, index=False) # Calculate parquet size if Path(parquet_path).is_dir(): parquet_size = sum(f.stat().st_size for f in Path(parquet_path).rglob('*.parquet')) / (1024 * 1024) else: parquet_size = Path(parquet_path).stat().st_size / (1024 * 1024) duration = (datetime.now() - start_time).total_seconds() result = ConversionResult( source_path=csv_path, output_path=parquet_path, source_format='csv', rows=len(df), columns=len(df.columns), original_size_mb=round(original_size, 2), parquet_size_mb=round(parquet_size, 2), compression_ratio=round(original_size / parquet_size, 2) if parquet_size > 0 else 0, duration_seconds=round(duration, 2) ) self.conversions.append(result) return result def excel_to_parquet(self, excel_path: str, parquet_path: str, sheet_name: Union[str, int] = 0, schema_name: str = None, compression: str = CompressionType.SNAPPY) -> ConversionResult: """Convert Excel to Parquet.""" start_time = datetime.now() # Read Excel df = pd.read_excel(excel_path, sheet_name=sheet_name) # Apply schema if schema_name and schema_name in self.schemas: df = self._apply_schema(df, self.schemas[schema_name]) original_size = Path(excel_path).stat().st_size / (1024 * 1024) # Write Parquet df.to_parquet(parquet_path, engine='pyarrow', compression=compression, index=False) parquet_size = Path(parquet_path).stat().st_size / (1024 * 1024) duration = (datetime.now() - start_time).total_seconds() result = ConversionResult( source_path=excel_path, output_path=parquet_path, source_format='excel', rows=len(df), columns=len(df.columns), original_size_mb=round(original_size, 2), parquet_size_mb=round(parquet_size, 2), compression_ratio=round(original_size / parquet_size, 2) if parquet_size > 0 else 0, duration_seconds=round(duration, 2) ) self.conversions.append(result) return result def json_to_parquet(self, json_path: str, parquet_path: str, schema_name: str = None, compression: str = CompressionType.SNAPPY) -> ConversionResult: """Convert JSON to Parquet.""" start_time = datetime.now() # Read JSON df = pd.read_json(json_path) if schema_name and schema_name in self.schemas: df = self._apply_schema(df, self.schemas[schema_name]) original_size = Path(json_path).stat().st_size / (1024 * 1024) df.to_parquet(parquet_path, engine='pyarrow', compression=compression, index=False) parquet_size = Path(parquet_path).stat().st_size / (1024 * 1024) duration = (datetime.now() - start_time).total_seconds() result = ConversionResult( source_path=json_path, output_path=parquet_path, source_format='json', rows=len(df), columns=len(df.columns), original_size_mb=round(original_size, 2), parquet_size_mb=round(parquet_size, 2), compression_ratio=round(original_size / parquet_size, 2) if parquet_size > 0 else 0, duration_seconds=round(duration, 2) ) self.conversions.append(result) return result def parquet_to_csv(self, parquet_path: str, csv_path: str) -> ConversionResult: """Convert Parquet to CSV.""" start_time = datetime.now() df = pd.read_parquet(parquet_path) if Path(parquet_path).is_dir(): original_size = sum(f.stat().st_size for f in Path(parquet_path).rglob('*.parquet')) / (1024 * 1024) else: original_size = Path(parquet_path).stat().st_size / (1024 * 1024) df.to_csv(csv_path, index=False) csv_size = Path(csv_path).stat().st_size / (1024 * 1024) duration = (datetime.now() - start_time).total_seconds() result = ConversionResult( source_path=parquet_path, output_path=csv_path, source_format='parquet', rows=len(df), columns=len(df.columns), original_size_mb=round(original_size, 2), parquet_size_mb=round(csv_size, 2), # Actually CSV size compression_ratio=round(csv_size / original_size, 2) if original_size > 0 else 0, duration_seconds=round(duration, 2) ) self.conversions.append(result) return result def _apply_schema(self, df: pd.DataFrame, schema: ParquetSchema) -> pd.DataFrame: """Apply schema to DataFrame.""" for col, dtype in schema.columns.items(): if col in df.columns: try: if dtype == 'category': df[col] = df[col].astype('category') elif dtype.startswith('datetime'): df[col] = pd.to_datetime(df[col]) else: df[col] = df[col].astype(dtype) except (ValueError, TypeError): pass # Keep original type if conversion fails return df def get_parquet_info(self, parquet_path: str) -> Dict[str, Any]: """Get information about a Parquet file.""" import pyarrow.parquet as pq if Path(parquet_path).is_dir(): # Partitioned dataset files = list(Path(parquet_path).rglob('*.parquet')) total_size = sum(f.stat().st_size for f in files) / (1024 * 1024) if files: sample = pq.read_table(str(files[0])) schema = sample.schema else: return {'error': 'No parquet files found'} return { 'path': parquet_path, 'type': 'partitioned', 'num_files': len(files), 'total_size_mb': round(total_size, 2), 'columns': [f.name for f in schema], 'dtypes': {f.name: str(f.type) for f in schema} } else: # Single file pf = pq.ParquetFile(parquet_path) metadata = pf.metadata return { 'path': parquet_path, 'type': 'single_file', 'size_mb': round(Path(parquet_path).stat().st_size / (1024 * 1024), 2), 'num_rows': metadata.num_rows, 'num_columns': metadata.num_columns, 'num_row_groups': metadata.num_row_groups, 'columns': [pf.schema_arrow.field(i).name for i in range(metadata.num_columns)], 'created_by': metadata.created_by } def query_parquet(self, parquet_path: str, columns: List[str] = None, filters: List[tuple] = None) -> pd.DataFrame: """Query Parquet file with column selection and filtering.""" return pd.read_parquet( parquet_path, columns=columns, filters=filters ) def merge_parquet_files(self, input_paths: List[str], output_path: str, compression: str = CompressionType.SNAPPY) -> ConversionResult: """Merge multiple Parquet files into one.""" start_time = datetime.now() dfs = [pd.read_parquet(p) for p in input_paths] merged = pd.concat(dfs, ignore_index=True) original_size = sum(Path(p).stat().st_size for p in input_paths) / (1024 * 1024) merged.to_parquet(output_path, engine='pyarrow', compression=compression, index=False) parquet_size = Path(output_path).stat().st_size / (1024 * 1024) duration = (datetime.now() - start_time).total_seconds() return ConversionResult( source_path=str(input_paths), output_path=output_path, source_format='parquet_merge', rows=len(merged), columns=len(merged.columns), original_size_mb=round(original_size, 2), parquet_size_mb=round(parquet_size, 2), compression_ratio=round(original_size / parquet_size, 2) if parquet_size > 0 else 0, duration_seconds=round(duration, 2) ) def get_conversion_summary(self) -> Dict[str, Any]: """Get summary of all conversions.""" if not self.conversions: return {'total_conversions': 0} return { 'total_conversions': len(self.conversions), 'total_rows_processed': sum(c.rows for c in self.conversions), 'original_size_mb': sum(c.original_size_mb for c in self.conversions), 'parquet_size_mb': sum(c.parquet_size_mb for c in self.conversions), 'avg_compression_ratio': round( sum(c.compression_ratio for c in self.conversions) / len(self.conversions), 2 ), 'total_duration_seconds': sum(c.duration_seconds for c in self.conversions) } def export_conversion_log(self, output_path: str) -> str: """Export conversion log to Excel.""" with pd.ExcelWriter(output_path, engine='openpyxl') as writer: # Summary summary = self.get_conversion_summary() summary_df = pd.DataFrame([summary]) summary_df.to_excel(writer, sheet_name='Summary', index=False) # Detailed log log_df = pd.DataFrame([{ 'Source': c.source_path, 'Output': c.output_path, 'Format': c.source_format, 'Rows': c.rows, 'Columns': c.columns, 'Original Size (MB)': c.original_size_mb, 'Parquet Size (MB)': c.parquet_size_mb, 'Compression Ratio': c.compression_ratio, 'Duration (s)': c.duration_seconds } for c in self.conversions]) log_df.to_excel(writer, sheet_name='Conversions', index=False) return output_path
python# Create converter converter = ParquetConverter("Project Data Migration") # Convert CSV to Parquet result = converter.csv_to_parquet( "costs.csv", "costs.parquet", schema_name="costs", compression="snappy" ) print(f"Converted {result.rows} rows") print(f"Compression ratio: {result.compression_ratio}x") print(f"Size: {result.original_size_mb}MB -> {result.parquet_size_mb}MB")
pythonresult = converter.excel_to_parquet( "project_data.xlsx", "project_data.parquet", schema_name="projects" )
python# Select specific columns with filter df = converter.query_parquet( "costs.parquet", columns=['project_id', 'amount', 'transaction_date'], filters=[('amount', '>', 10000)] )
pythoninfo = converter.get_parquet_info("costs.parquet") print(f"Rows: {info['num_rows']}") print(f"Columns: {info['columns']}")
pythonresult = converter.merge_parquet_files( ["costs_2023.parquet", "costs_2024.parquet"], "costs_all.parquet" )
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | fail→fail | 20,763 | 18,336 | -12% | 1 | 1 | 0% | 4,029 | 8,436 | +109% | 0 | 0 | — |
case-01 | fail→pass | 7,970 | 6,132 | -23% | 1 | 1 | 0% | 1,563 | 5,978 | +282% | 0 | 0 | — |
case-02 | fail→pass | 20,250 | 8,241 | -59% | 1 | 1 | 0% | 4,538 | 6,217 | +37% | 0 | 0 | — |
case-03 | fail→pass | 7,941 | 6,678 | -16% | 1 | 1 | 0% | 1,650 | 5,878 | +256% | 0 | 0 | — |
case-04 | fail→pass | 13,234 | 4,169 | -68% | 1 | 1 | 0% | 2,125 | 5,359 | +152% | 0 | 0 | — |
case-05 | fail→pass | 13,187 | 3,320 | -75% | 1 | 1 | 0% | 2,202 | 5,248 | +138% | 0 | 0 | — |
case-06 | pass→pass | 5,490 | 2,224 | -59% | 1 | 1 | 0% | 871 | 5,032 | +478% | 0 | 0 | — |
case-07 | fail→pass | 11,288 | 5,262 | -53% | 1 | 1 | 0% | 1,957 | 5,703 | +191% | 0 | 0 | — |
case-08 | fail→pass | 5,064 | 5,852 | +16% | 1 | 1 | 0% | 1,000 | 5,770 | +477% | 0 | 0 | — |
case-09 | fail→fail | 6,707 | 5,520 | -18% | 1 | 1 | 0% | 1,178 | 5,659 | +380% | 0 | 0 | — |
case-10 | fail→pass | 10,536 | 6,172 | -41% | 1 | 1 | 0% | 1,917 | 5,724 | +199% | 0 | 0 | — |
case-11 | pass→pass | 15,216 | 8,210 | -46% | 1 | 1 | 0% | 2,653 | 6,283 | +137% | 0 | 0 | — |
case-12 | pass→pass | 13,081 | 5,094 | -61% | 1 | 1 | 0% | 2,370 | 5,626 | +137% | 0 | 0 | — |
case-13 | fail→pass | 6,775 | 6,437 | -5% | 1 | 1 | 0% | 1,285 | 5,888 | +358% | 0 | 0 | — |
case-14 | fail→pass | 11,733 | 3,500 | -70% | 1 | 1 | 0% | 1,750 | 5,231 | +199% | 0 | 0 | — |
case-15 | fail→fail | 6,323 | 5,803 | -8% | 1 | 1 | 0% | 1,289 | 5,790 | +349% | 0 | 0 | — |
case-16 | fail→pass | 12,302 | 1,922 | -84% | 1 | 1 | 0% | 2,083 | 4,959 | +138% | 0 | 0 | — |
case-17 | fail→pass | 12,720 | 3,340 | -74% | 1 | 1 | 0% | 2,078 | 5,219 | +151% | 0 | 0 | — |
case-18 | pass→pass | 15,024 | 23,094 | +54% | 1 | 1 | 0% | 2,505 | 6,428 | +157% | 0 | 0 | — |
case-19 | fail→pass | 10,977 | 2,927 | -73% | 1 | 1 | 0% | 1,933 | 5,200 | +169% | 0 | 0 | — |
case-20 | fail→fail | 15,427 | 16,734 | +8% | 1 | 1 | 0% | 2,781 | 8,032 | +189% | 0 | 0 | — |
case-21 | fail→fail | 15,817 | 17,509 | +11% | 1 | 1 | 0% | 3,054 | 8,080 | +165% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +59 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.