eda_analyzer.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. #!/usr/bin/env python3
  2. """
  3. Exploratory Data Analysis Analyzer
  4. Analyzes scientific data files and generates comprehensive markdown reports
  5. """
  6. import os
  7. import sys
  8. from pathlib import Path
  9. from datetime import datetime
  10. import json
  11. def detect_file_type(filepath):
  12. """
  13. Detect the file type based on extension and content.
  14. Returns:
  15. tuple: (extension, file_category, reference_file)
  16. """
  17. file_path = Path(filepath)
  18. extension = file_path.suffix.lower()
  19. name = file_path.name.lower()
  20. # Map extensions to categories and reference files
  21. extension_map = {
  22. # Chemistry/Molecular
  23. 'pdb': ('chemistry_molecular', 'Protein Data Bank'),
  24. 'cif': ('chemistry_molecular', 'Crystallographic Information File'),
  25. 'mol': ('chemistry_molecular', 'MDL Molfile'),
  26. 'mol2': ('chemistry_molecular', 'Tripos Mol2'),
  27. 'sdf': ('chemistry_molecular', 'Structure Data File'),
  28. 'xyz': ('chemistry_molecular', 'XYZ Coordinates'),
  29. 'smi': ('chemistry_molecular', 'SMILES String'),
  30. 'smiles': ('chemistry_molecular', 'SMILES String'),
  31. 'pdbqt': ('chemistry_molecular', 'AutoDock PDBQT'),
  32. 'mae': ('chemistry_molecular', 'Maestro Format'),
  33. 'gro': ('chemistry_molecular', 'GROMACS Coordinate File'),
  34. 'log': ('chemistry_molecular', 'Gaussian Log File'),
  35. 'out': ('chemistry_molecular', 'Quantum Chemistry Output'),
  36. 'wfn': ('chemistry_molecular', 'Wavefunction Files'),
  37. 'wfx': ('chemistry_molecular', 'Wavefunction Files'),
  38. 'fchk': ('chemistry_molecular', 'Gaussian Formatted Checkpoint'),
  39. 'cube': ('chemistry_molecular', 'Gaussian Cube File'),
  40. 'dcd': ('chemistry_molecular', 'Binary Trajectory'),
  41. 'xtc': ('chemistry_molecular', 'Compressed Trajectory'),
  42. 'trr': ('chemistry_molecular', 'GROMACS Trajectory'),
  43. 'nc': ('chemistry_molecular', 'Amber NetCDF Trajectory'),
  44. 'netcdf': ('chemistry_molecular', 'Amber NetCDF Trajectory'),
  45. # Bioinformatics/Genomics
  46. 'fasta': ('bioinformatics_genomics', 'FASTA Format'),
  47. 'fa': ('bioinformatics_genomics', 'FASTA Format'),
  48. 'fna': ('bioinformatics_genomics', 'FASTA Format'),
  49. 'fastq': ('bioinformatics_genomics', 'FASTQ Format'),
  50. 'fq': ('bioinformatics_genomics', 'FASTQ Format'),
  51. 'sam': ('bioinformatics_genomics', 'Sequence Alignment/Map'),
  52. 'bam': ('bioinformatics_genomics', 'Binary Alignment/Map'),
  53. 'cram': ('bioinformatics_genomics', 'CRAM Format'),
  54. 'bed': ('bioinformatics_genomics', 'Browser Extensible Data'),
  55. 'bedgraph': ('bioinformatics_genomics', 'BED with Graph Data'),
  56. 'bigwig': ('bioinformatics_genomics', 'Binary BigWig'),
  57. 'bw': ('bioinformatics_genomics', 'Binary BigWig'),
  58. 'bigbed': ('bioinformatics_genomics', 'Binary BigBed'),
  59. 'bb': ('bioinformatics_genomics', 'Binary BigBed'),
  60. 'gff': ('bioinformatics_genomics', 'General Feature Format'),
  61. 'gff3': ('bioinformatics_genomics', 'General Feature Format'),
  62. 'gtf': ('bioinformatics_genomics', 'Gene Transfer Format'),
  63. 'vcf': ('bioinformatics_genomics', 'Variant Call Format'),
  64. 'bcf': ('bioinformatics_genomics', 'Binary VCF'),
  65. 'gvcf': ('bioinformatics_genomics', 'Genomic VCF'),
  66. # Microscopy/Imaging
  67. 'tif': ('microscopy_imaging', 'Tagged Image File Format'),
  68. 'tiff': ('microscopy_imaging', 'Tagged Image File Format'),
  69. 'nd2': ('microscopy_imaging', 'Nikon NIS-Elements'),
  70. 'lif': ('microscopy_imaging', 'Leica Image Format'),
  71. 'czi': ('microscopy_imaging', 'Carl Zeiss Image'),
  72. 'oib': ('microscopy_imaging', 'Olympus Image Format'),
  73. 'oif': ('microscopy_imaging', 'Olympus Image Format'),
  74. 'vsi': ('microscopy_imaging', 'Olympus VSI'),
  75. 'ims': ('microscopy_imaging', 'Imaris Format'),
  76. 'lsm': ('microscopy_imaging', 'Zeiss LSM'),
  77. 'stk': ('microscopy_imaging', 'MetaMorph Stack'),
  78. 'dv': ('microscopy_imaging', 'DeltaVision'),
  79. 'mrc': ('microscopy_imaging', 'Medical Research Council'),
  80. 'dm3': ('microscopy_imaging', 'Gatan Digital Micrograph'),
  81. 'dm4': ('microscopy_imaging', 'Gatan Digital Micrograph'),
  82. 'dcm': ('microscopy_imaging', 'DICOM'),
  83. 'nii': ('microscopy_imaging', 'NIfTI'),
  84. 'nrrd': ('microscopy_imaging', 'Nearly Raw Raster Data'),
  85. # Spectroscopy/Analytical
  86. 'fid': ('spectroscopy_analytical', 'NMR Free Induction Decay'),
  87. 'mzml': ('spectroscopy_analytical', 'Mass Spectrometry Markup Language'),
  88. 'mzxml': ('spectroscopy_analytical', 'Mass Spectrometry XML'),
  89. 'raw': ('spectroscopy_analytical', 'Vendor Raw Files'),
  90. 'd': ('spectroscopy_analytical', 'Agilent Data Directory'),
  91. 'mgf': ('spectroscopy_analytical', 'Mascot Generic Format'),
  92. 'spc': ('spectroscopy_analytical', 'Galactic SPC'),
  93. 'jdx': ('spectroscopy_analytical', 'JCAMP-DX'),
  94. 'jcamp': ('spectroscopy_analytical', 'JCAMP-DX'),
  95. # Proteomics/Metabolomics
  96. 'pepxml': ('proteomics_metabolomics', 'Trans-Proteomic Pipeline Peptide XML'),
  97. 'protxml': ('proteomics_metabolomics', 'Protein Inference Results'),
  98. 'mzid': ('proteomics_metabolomics', 'Peptide Identification Format'),
  99. 'mztab': ('proteomics_metabolomics', 'Proteomics/Metabolomics Tabular Format'),
  100. # General Scientific
  101. 'npy': ('general_scientific', 'NumPy Array'),
  102. 'npz': ('general_scientific', 'Compressed NumPy Archive'),
  103. 'csv': ('general_scientific', 'Comma-Separated Values'),
  104. 'tsv': ('general_scientific', 'Tab-Separated Values'),
  105. 'xlsx': ('general_scientific', 'Excel Spreadsheets'),
  106. 'xls': ('general_scientific', 'Excel Spreadsheets'),
  107. 'json': ('general_scientific', 'JavaScript Object Notation'),
  108. 'xml': ('general_scientific', 'Extensible Markup Language'),
  109. 'hdf5': ('general_scientific', 'Hierarchical Data Format 5'),
  110. 'h5': ('general_scientific', 'Hierarchical Data Format 5'),
  111. 'h5ad': ('bioinformatics_genomics', 'Anndata Format'),
  112. 'zarr': ('general_scientific', 'Chunked Array Storage'),
  113. 'parquet': ('general_scientific', 'Apache Parquet'),
  114. 'mat': ('general_scientific', 'MATLAB Data'),
  115. 'fits': ('general_scientific', 'Flexible Image Transport System'),
  116. }
  117. ext_clean = extension.lstrip('.')
  118. if ext_clean in extension_map:
  119. category, description = extension_map[ext_clean]
  120. return ext_clean, category, description
  121. return ext_clean, 'unknown', 'Unknown Format'
  122. def get_file_basic_info(filepath):
  123. """Get basic file information."""
  124. file_path = Path(filepath)
  125. stat = file_path.stat()
  126. return {
  127. 'filename': file_path.name,
  128. 'path': str(file_path.absolute()),
  129. 'size_bytes': stat.st_size,
  130. 'size_human': format_bytes(stat.st_size),
  131. 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
  132. 'extension': file_path.suffix.lower(),
  133. }
  134. def format_bytes(size):
  135. """Convert bytes to human-readable format."""
  136. for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
  137. if size < 1024.0:
  138. return f"{size:.2f} {unit}"
  139. size /= 1024.0
  140. return f"{size:.2f} PB"
  141. def load_reference_info(category, extension):
  142. """
  143. Load reference information for the file type.
  144. Args:
  145. category: File category (e.g., 'chemistry_molecular')
  146. extension: File extension
  147. Returns:
  148. dict: Reference information
  149. """
  150. # Map categories to reference files
  151. category_files = {
  152. 'chemistry_molecular': 'chemistry_molecular_formats.md',
  153. 'bioinformatics_genomics': 'bioinformatics_genomics_formats.md',
  154. 'microscopy_imaging': 'microscopy_imaging_formats.md',
  155. 'spectroscopy_analytical': 'spectroscopy_analytical_formats.md',
  156. 'proteomics_metabolomics': 'proteomics_metabolomics_formats.md',
  157. 'general_scientific': 'general_scientific_formats.md',
  158. }
  159. if category not in category_files:
  160. return None
  161. # Get the reference file path
  162. script_dir = Path(__file__).parent
  163. ref_file = script_dir.parent / 'references' / category_files[category]
  164. if not ref_file.exists():
  165. return None
  166. # Parse the reference file for the specific extension
  167. # This is a simplified parser - could be more sophisticated
  168. try:
  169. with open(ref_file, 'r') as f:
  170. content = f.read()
  171. # Extract section for this file type
  172. # Look for the extension heading
  173. import re
  174. pattern = rf'### \.{extension}[^#]*?(?=###|\Z)'
  175. match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
  176. if match:
  177. section = match.group(0)
  178. return {
  179. 'raw_section': section,
  180. 'reference_file': category_files[category]
  181. }
  182. except Exception as e:
  183. print(f"Error loading reference: {e}", file=sys.stderr)
  184. return None
  185. def analyze_file(filepath):
  186. """
  187. Main analysis function that routes to specific analyzers.
  188. Returns:
  189. dict: Analysis results
  190. """
  191. basic_info = get_file_basic_info(filepath)
  192. extension, category, description = detect_file_type(filepath)
  193. analysis = {
  194. 'basic_info': basic_info,
  195. 'file_type': {
  196. 'extension': extension,
  197. 'category': category,
  198. 'description': description
  199. },
  200. 'reference_info': load_reference_info(category, extension),
  201. 'data_analysis': {}
  202. }
  203. # Try to perform data-specific analysis based on file type
  204. try:
  205. if category == 'general_scientific':
  206. analysis['data_analysis'] = analyze_general_scientific(filepath, extension)
  207. elif category == 'bioinformatics_genomics':
  208. analysis['data_analysis'] = analyze_bioinformatics(filepath, extension)
  209. elif category == 'microscopy_imaging':
  210. analysis['data_analysis'] = analyze_imaging(filepath, extension)
  211. # Add more specific analyzers as needed
  212. except Exception as e:
  213. analysis['data_analysis']['error'] = str(e)
  214. return analysis
  215. def analyze_general_scientific(filepath, extension):
  216. """Analyze general scientific data formats."""
  217. results = {}
  218. try:
  219. if extension in ['npy']:
  220. import numpy as np
  221. data = np.load(filepath)
  222. results = {
  223. 'shape': data.shape,
  224. 'dtype': str(data.dtype),
  225. 'size': data.size,
  226. 'ndim': data.ndim,
  227. 'statistics': {
  228. 'min': float(np.min(data)) if np.issubdtype(data.dtype, np.number) else None,
  229. 'max': float(np.max(data)) if np.issubdtype(data.dtype, np.number) else None,
  230. 'mean': float(np.mean(data)) if np.issubdtype(data.dtype, np.number) else None,
  231. 'std': float(np.std(data)) if np.issubdtype(data.dtype, np.number) else None,
  232. }
  233. }
  234. elif extension in ['npz']:
  235. import numpy as np
  236. data = np.load(filepath)
  237. results = {
  238. 'arrays': list(data.files),
  239. 'array_count': len(data.files),
  240. 'array_shapes': {name: data[name].shape for name in data.files}
  241. }
  242. elif extension in ['csv', 'tsv']:
  243. import pandas as pd
  244. sep = '\t' if extension == 'tsv' else ','
  245. df = pd.read_csv(filepath, sep=sep, nrows=10000) # Sample first 10k rows
  246. results = {
  247. 'shape': df.shape,
  248. 'columns': list(df.columns),
  249. 'dtypes': {col: str(dtype) for col, dtype in df.dtypes.items()},
  250. 'missing_values': df.isnull().sum().to_dict(),
  251. 'summary_statistics': df.describe().to_dict() if len(df.select_dtypes(include='number').columns) > 0 else {}
  252. }
  253. elif extension in ['json']:
  254. with open(filepath, 'r') as f:
  255. data = json.load(f)
  256. results = {
  257. 'type': type(data).__name__,
  258. 'keys': list(data.keys()) if isinstance(data, dict) else None,
  259. 'length': len(data) if isinstance(data, (list, dict)) else None
  260. }
  261. elif extension in ['h5', 'hdf5']:
  262. import h5py
  263. with h5py.File(filepath, 'r') as f:
  264. def get_structure(group, prefix=''):
  265. items = {}
  266. for key in group.keys():
  267. path = f"{prefix}/{key}"
  268. if isinstance(group[key], h5py.Dataset):
  269. items[path] = {
  270. 'type': 'dataset',
  271. 'shape': group[key].shape,
  272. 'dtype': str(group[key].dtype)
  273. }
  274. elif isinstance(group[key], h5py.Group):
  275. items[path] = {'type': 'group'}
  276. items.update(get_structure(group[key], path))
  277. return items
  278. results = {
  279. 'structure': get_structure(f),
  280. 'attributes': dict(f.attrs)
  281. }
  282. except ImportError as e:
  283. results['error'] = f"Required library not installed: {e}"
  284. except Exception as e:
  285. results['error'] = f"Analysis error: {e}"
  286. return results
  287. def analyze_bioinformatics(filepath, extension):
  288. """Analyze bioinformatics/genomics formats."""
  289. results = {}
  290. try:
  291. if extension in ['fasta', 'fa', 'fna']:
  292. from Bio import SeqIO
  293. sequences = list(SeqIO.parse(filepath, 'fasta'))
  294. lengths = [len(seq) for seq in sequences]
  295. results = {
  296. 'sequence_count': len(sequences),
  297. 'total_length': sum(lengths),
  298. 'mean_length': sum(lengths) / len(lengths) if lengths else 0,
  299. 'min_length': min(lengths) if lengths else 0,
  300. 'max_length': max(lengths) if lengths else 0,
  301. 'sequence_ids': [seq.id for seq in sequences[:10]] # First 10
  302. }
  303. elif extension in ['fastq', 'fq']:
  304. from Bio import SeqIO
  305. sequences = []
  306. for i, seq in enumerate(SeqIO.parse(filepath, 'fastq')):
  307. sequences.append(seq)
  308. if i >= 9999: # Sample first 10k
  309. break
  310. lengths = [len(seq) for seq in sequences]
  311. qualities = [sum(seq.letter_annotations['phred_quality']) / len(seq) for seq in sequences]
  312. results = {
  313. 'read_count_sampled': len(sequences),
  314. 'mean_length': sum(lengths) / len(lengths) if lengths else 0,
  315. 'mean_quality': sum(qualities) / len(qualities) if qualities else 0,
  316. 'min_length': min(lengths) if lengths else 0,
  317. 'max_length': max(lengths) if lengths else 0,
  318. }
  319. except ImportError as e:
  320. results['error'] = f"Required library not installed (try: pip install biopython): {e}"
  321. except Exception as e:
  322. results['error'] = f"Analysis error: {e}"
  323. return results
  324. def analyze_imaging(filepath, extension):
  325. """Analyze microscopy/imaging formats."""
  326. results = {}
  327. try:
  328. if extension in ['tif', 'tiff', 'png', 'jpg', 'jpeg']:
  329. from PIL import Image
  330. import numpy as np
  331. img = Image.open(filepath)
  332. img_array = np.array(img)
  333. results = {
  334. 'size': img.size,
  335. 'mode': img.mode,
  336. 'format': img.format,
  337. 'shape': img_array.shape,
  338. 'dtype': str(img_array.dtype),
  339. 'value_range': [int(img_array.min()), int(img_array.max())],
  340. 'mean_intensity': float(img_array.mean()),
  341. }
  342. # Check for multi-page TIFF
  343. if extension in ['tif', 'tiff']:
  344. try:
  345. frame_count = 0
  346. while True:
  347. img.seek(frame_count)
  348. frame_count += 1
  349. except EOFError:
  350. results['page_count'] = frame_count
  351. except ImportError as e:
  352. results['error'] = f"Required library not installed (try: pip install pillow): {e}"
  353. except Exception as e:
  354. results['error'] = f"Analysis error: {e}"
  355. return results
  356. def generate_markdown_report(analysis, output_path=None):
  357. """
  358. Generate a comprehensive markdown report from analysis results.
  359. Args:
  360. analysis: Analysis results dictionary
  361. output_path: Path to save the report (if None, prints to stdout)
  362. """
  363. lines = []
  364. # Title
  365. filename = analysis['basic_info']['filename']
  366. lines.append(f"# Exploratory Data Analysis Report: {filename}\n")
  367. lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
  368. lines.append("---\n")
  369. # Basic Information
  370. lines.append("## Basic Information\n")
  371. basic = analysis['basic_info']
  372. lines.append(f"- **Filename:** `{basic['filename']}`")
  373. lines.append(f"- **Full Path:** `{basic['path']}`")
  374. lines.append(f"- **File Size:** {basic['size_human']} ({basic['size_bytes']:,} bytes)")
  375. lines.append(f"- **Last Modified:** {basic['modified']}")
  376. lines.append(f"- **Extension:** `.{analysis['file_type']['extension']}`\n")
  377. # File Type Information
  378. lines.append("## File Type\n")
  379. ft = analysis['file_type']
  380. lines.append(f"- **Category:** {ft['category'].replace('_', ' ').title()}")
  381. lines.append(f"- **Description:** {ft['description']}\n")
  382. # Reference Information
  383. if analysis.get('reference_info'):
  384. lines.append("## Format Reference\n")
  385. ref = analysis['reference_info']
  386. if 'raw_section' in ref:
  387. lines.append(ref['raw_section'])
  388. lines.append(f"\n*Reference: {ref['reference_file']}*\n")
  389. # Data Analysis
  390. if analysis.get('data_analysis'):
  391. lines.append("## Data Analysis\n")
  392. data = analysis['data_analysis']
  393. if 'error' in data:
  394. lines.append(f"⚠️ **Analysis Error:** {data['error']}\n")
  395. else:
  396. # Format the data analysis based on what's present
  397. lines.append("### Summary Statistics\n")
  398. lines.append("```json")
  399. lines.append(json.dumps(data, indent=2, default=str))
  400. lines.append("```\n")
  401. # Recommendations
  402. lines.append("## Recommendations for Further Analysis\n")
  403. lines.append(f"Based on the file type (`.{analysis['file_type']['extension']}`), consider the following analyses:\n")
  404. # Add specific recommendations based on category
  405. category = analysis['file_type']['category']
  406. if category == 'general_scientific':
  407. lines.append("- Statistical distribution analysis")
  408. lines.append("- Missing value imputation strategies")
  409. lines.append("- Correlation analysis between variables")
  410. lines.append("- Outlier detection and handling")
  411. lines.append("- Dimensionality reduction (PCA, t-SNE)")
  412. elif category == 'bioinformatics_genomics':
  413. lines.append("- Sequence quality control and filtering")
  414. lines.append("- GC content analysis")
  415. lines.append("- Read alignment and mapping statistics")
  416. lines.append("- Variant calling and annotation")
  417. lines.append("- Differential expression analysis")
  418. elif category == 'microscopy_imaging':
  419. lines.append("- Image quality assessment")
  420. lines.append("- Background correction and normalization")
  421. lines.append("- Segmentation and object detection")
  422. lines.append("- Colocalization analysis")
  423. lines.append("- Intensity measurements and quantification")
  424. lines.append("")
  425. # Footer
  426. lines.append("---")
  427. lines.append("*This report was generated by the exploratory-data-analysis skill.*")
  428. report = '\n'.join(lines)
  429. if output_path:
  430. with open(output_path, 'w') as f:
  431. f.write(report)
  432. print(f"Report saved to: {output_path}")
  433. else:
  434. print(report)
  435. return report
  436. def main():
  437. """Main CLI interface."""
  438. if len(sys.argv) < 2:
  439. print("Usage: python eda_analyzer.py <filepath> [output.md]")
  440. print(" filepath: Path to the data file to analyze")
  441. print(" output.md: Optional output path for markdown report")
  442. sys.exit(1)
  443. filepath = sys.argv[1]
  444. output_path = sys.argv[2] if len(sys.argv) > 2 else None
  445. if not os.path.exists(filepath):
  446. print(f"Error: File not found: {filepath}")
  447. sys.exit(1)
  448. # If no output path specified, use the input filename
  449. if output_path is None:
  450. input_path = Path(filepath)
  451. output_path = input_path.parent / f"{input_path.stem}_eda_report.md"
  452. print(f"Analyzing: {filepath}")
  453. analysis = analyze_file(filepath)
  454. print(f"\nGenerating report...")
  455. generate_markdown_report(analysis, output_path)
  456. print(f"\n✓ Analysis complete!")
  457. if __name__ == '__main__':
  458. main()