{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":116062,"databundleVersionId":14875579,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# NOTEBOOK 1: Data Exploration & Preprocessing\n# CAFA 6 Protein Function Prediction\n# Hardware: CPU | Estimated Time: 1-2 hours\n\nimport os\nimport pickle\nimport pandas as pd\nimport numpy as np\nfrom pathlib import Path\nfrom collections import defaultdict, Counter\nimport networkx as nx\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ============================================================================\n# SETUP & PATHS\n# ============================================================================\n\n# Input paths\nDATA_PATH = Path('/kaggle/input/cafa-6-protein-function-prediction')\nTRAIN_PATH = DATA_PATH / 'Train'\nTEST_PATH = DATA_PATH / 'Test'\n\n# Output directory\nOUTPUT_DIR = Path('/kaggle/working/outputs')\nOUTPUT_DIR.mkdir(exist_ok=True)\n\nprint(\"=\" * 80)\nprint(\"CAFA 6 - NOTEBOOK 1: DATA EXPLORATION & PREPROCESSING\")\nprint(\"=\" * 80)\nprint(f\"\\n📁 Output Directory: {OUTPUT_DIR}\")\n\n# ============================================================================\n# 1. PARSE GO ONTOLOGY (go-basic.obo)\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"1. PARSING GO ONTOLOGY\")\nprint(\"=\" * 80)\n\ndef parse_obo_file(obo_file):\n    \"\"\"Parse GO OBO file and create graph structure\"\"\"\n    \n    print(f\"Reading {obo_file}...\")\n    \n    # NetworkX graph for GO structure\n    go_graph = nx.DiGraph()\n    \n    # Dictionaries for term information\n    term_info = {}\n    \n    current_term = None\n    current_data = {}\n    \n    with open(obo_file, 'r') as f:\n        for line in f:\n            line = line.strip()\n            \n            if line == \"[Term]\":\n                # Save previous term if exists\n                if current_term:\n                    term_info[current_term] = current_data\n                    \n                current_data = {\n                    'name': '',\n                    'namespace': '',\n                    'is_obsolete': False,\n                    'parents': [],\n                    'alt_ids': []\n                }\n                current_term = None\n                \n            elif line.startswith(\"id:\"):\n                current_term = line.split(\"id: \")[1]\n                \n            elif line.startswith(\"name:\"):\n                current_data['name'] = line.split(\"name: \")[1]\n                \n            elif line.startswith(\"namespace:\"):\n                namespace = line.split(\"namespace: \")[1]\n                current_data['namespace'] = namespace\n                \n            elif line.startswith(\"is_a:\"):\n                parent = line.split(\"is_a: \")[1].split(\" !\")[0]\n                current_data['parents'].append(parent)\n                \n            elif line.startswith(\"is_obsolete:\"):\n                if \"true\" in line.lower():\n                    current_data['is_obsolete'] = True\n                    \n            elif line.startswith(\"alt_id:\"):\n                alt_id = line.split(\"alt_id: \")[1]\n                current_data['alt_ids'].append(alt_id)\n    \n    # Save last term\n    if current_term:\n        term_info[current_term] = current_data\n    \n    # Build graph\n    print(\"Building GO graph...\")\n    for term, data in tqdm(term_info.items(), desc=\"Adding nodes/edges\"):\n        if not data['is_obsolete']:\n            go_graph.add_node(term, **data)\n            \n            # Add edges from child to parent (is_a relationship)\n            for parent in data['parents']:\n                if parent in term_info and not term_info[parent]['is_obsolete']:\n                    go_graph.add_edge(term, parent, relation='is_a')\n    \n    return go_graph, term_info\n\n# Parse GO file\nGO_FILE = TRAIN_PATH / 'go-basic.obo'\ngo_graph, term_info = parse_obo_file(GO_FILE)\n\nprint(f\"\\n✓ GO Graph Created:\")\nprint(f\"  - Total nodes: {go_graph.number_of_nodes():,}\")\nprint(f\"  - Total edges: {go_graph.number_of_edges():,}\")\n\n# Separate by ontology\nsubontology_roots = {\n    'BPO': 'GO:0008150',  # Biological Process\n    'CCO': 'GO:0005575',  # Cellular Component\n    'MFO': 'GO:0003674'   # Molecular Function\n}\n\nontology_terms = {ont: set() for ont in ['BPO', 'CCO', 'MFO']}\nfor term, data in term_info.items():\n    if not data['is_obsolete']:\n        namespace = data['namespace']\n        if 'biological_process' in namespace:\n            ontology_terms['BPO'].add(term)\n        elif 'cellular_component' in namespace:\n            ontology_terms['CCO'].add(term)\n        elif 'molecular_function' in namespace:\n            ontology_terms['MFO'].add(term)\n\nprint(f\"\\n📊 Terms by Ontology:\")\nfor ont, terms in ontology_terms.items():\n    print(f\"  - {ont}: {len(terms):,} terms\")\n\n# Save GO graph\nwith open(OUTPUT_DIR / 'go_graph.pkl', 'wb') as f:\n    pickle.dump(go_graph, f)\nwith open(OUTPUT_DIR / 'term_info.pkl', 'wb') as f:\n    pickle.dump(term_info, f)\nwith open(OUTPUT_DIR / 'ontology_terms.pkl', 'wb') as f:\n    pickle.dump(ontology_terms, f)\n\nprint(\"\\n✓ Saved: go_graph.pkl, term_info.pkl, ontology_terms.pkl\")\n\n# ============================================================================\n# 2. LOAD INFORMATION ACCRETION (IA) WEIGHTS\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"2. LOADING INFORMATION ACCRETION WEIGHTS\")\nprint(\"=\" * 80)\n\nia_df = pd.read_csv(DATA_PATH / 'IA.tsv', sep='\\t', header=None, \n                     names=['term', 'ia_weight'])\n\nprint(f\"\\n✓ Loaded IA weights: {len(ia_df):,} terms\")\nprint(f\"\\nIA Weight Statistics:\")\nprint(ia_df['ia_weight'].describe())\n\n# Create dictionary for fast lookup\nia_weights = dict(zip(ia_df['term'], ia_df['ia_weight']))\n\n# Save\nwith open(OUTPUT_DIR / 'ia_weights.pkl', 'wb') as f:\n    pickle.dump(ia_weights, f)\n\nprint(\"\\n✓ Saved: ia_weights.pkl\")\n\n# ============================================================================\n# 3. LOAD TRAINING DATA\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"3. LOADING TRAINING DATA\")\nprint(\"=\" * 80)\n\n# Load training terms\nprint(\"Loading train_terms.tsv...\")\ntrain_terms_df = pd.read_csv(TRAIN_PATH / 'train_terms.tsv', sep='\\t', \n                               header=None, names=['protein_id', 'go_term', 'ontology'])\n\nprint(f\"✓ Loaded {len(train_terms_df):,} protein-term associations\")\nprint(f\"\\nOntology Distribution (raw):\")\nprint(train_terms_df['ontology'].value_counts())\n\n# Map ontology codes to full names: P->BPO, C->CCO, F->MFO\nontology_mapping = {\n    'P': 'BPO',  # Biological Process\n    'C': 'CCO',  # Cellular Component\n    'F': 'MFO',  # Molecular Function\n}\n\n# Filter out invalid ontology codes and map to standard names\nvalid_mask = train_terms_df['ontology'].isin(['P', 'C', 'F'])\ntrain_terms_df = train_terms_df[valid_mask].copy()\ntrain_terms_df['ontology'] = train_terms_df['ontology'].map(ontology_mapping)\n\nprint(f\"\\n✓ After filtering and mapping: {len(train_terms_df):,} associations\")\nprint(f\"\\nOntology Distribution (mapped):\")\nprint(train_terms_df['ontology'].value_counts())\n\n# Load taxonomy\nprint(\"\\nLoading train_taxonomy.tsv...\")\ntrain_taxonomy_df = pd.read_csv(TRAIN_PATH / 'train_taxonomy.tsv', sep='\\t',\n                                 header=None, names=['protein_id', 'taxon_id'])\n\nprint(f\"✓ Loaded taxonomy for {len(train_taxonomy_df):,} proteins\")\nprint(f\"  - Unique species: {train_taxonomy_df['taxon_id'].nunique():,}\")\n\n# Parse FASTA sequences\nprint(\"\\nParsing train_sequences.fasta...\")\n\ndef parse_fasta(fasta_file):\n    \"\"\"Parse FASTA file and return dict of sequences\"\"\"\n    sequences = {}\n    current_id = None\n    current_seq = []\n    \n    with open(fasta_file, 'r') as f:\n        for line in f:\n            line = line.strip()\n            if line.startswith('>'):\n                # Save previous sequence\n                if current_id:\n                    sequences[current_id] = ''.join(current_seq)\n                \n                # Parse header: >sp|P9WHI7|RECN_MYCTU ...\n                header = line[1:].split('|')\n                if len(header) >= 2:\n                    current_id = header[1]\n                else:\n                    current_id = line[1:].split()[0]\n                \n                current_seq = []\n            else:\n                current_seq.append(line)\n        \n        # Save last sequence\n        if current_id:\n            sequences[current_id] = ''.join(current_seq)\n    \n    return sequences\n\ntrain_sequences = parse_fasta(TRAIN_PATH / 'train_sequences.fasta')\nprint(f\"✓ Loaded {len(train_sequences):,} protein sequences\")\n\n# Sequence length statistics\nseq_lengths = [len(seq) for seq in train_sequences.values()]\nprint(f\"\\nSequence Length Statistics:\")\nprint(f\"  - Mean: {np.mean(seq_lengths):.1f}\")\nprint(f\"  - Median: {np.median(seq_lengths):.1f}\")\nprint(f\"  - Min: {np.min(seq_lengths)}\")\nprint(f\"  - Max: {np.max(seq_lengths)}\")\nprint(f\"  - Std: {np.std(seq_lengths):.1f}\")\n\n# Save\nwith open(OUTPUT_DIR / 'train_sequences.pkl', 'wb') as f:\n    pickle.dump(train_sequences, f)\n\nprint(\"\\n✓ Saved: train_sequences.pkl\")\n\n# ============================================================================\n# 4. TERM FREQUENCY ANALYSIS\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"4. TERM FREQUENCY ANALYSIS\")\nprint(\"=\" * 80)\n\n# Count proteins per term\nterm_frequencies = Counter(train_terms_df['go_term'])\nprint(f\"\\n✓ Unique GO terms in training: {len(term_frequencies):,}\")\n\n# Statistics\nfreq_values = list(term_frequencies.values())\nprint(f\"\\nTerm Frequency Statistics:\")\nprint(f\"  - Mean proteins per term: {np.mean(freq_values):.1f}\")\nprint(f\"  - Median proteins per term: {np.median(freq_values):.1f}\")\nprint(f\"  - Max proteins for a term: {np.max(freq_values):,}\")\nprint(f\"  - Min proteins for a term: {np.min(freq_values)}\")\n\n# Top 10 most frequent terms\nprint(\"\\nTop 10 Most Frequent GO Terms:\")\nfor term, count in term_frequencies.most_common(10):\n    term_name = term_info.get(term, {}).get('name', 'Unknown')\n    print(f\"  {term}: {count:,} proteins - {term_name}\")\n\n# Terms by ontology - FIX: Use mapped ontology codes\nterm_freq_by_ontology = {ont: {} for ont in ['BPO', 'CCO', 'MFO']}\nfor _, row in train_terms_df.iterrows():\n    ont = row['ontology']\n    term = row['go_term']\n    if ont in term_freq_by_ontology:  # Safety check\n        term_freq_by_ontology[ont][term] = term_freq_by_ontology[ont].get(term, 0) + 1\n\nprint(f\"\\nUnique Terms by Ontology:\")\nfor ont, freqs in term_freq_by_ontology.items():\n    print(f\"  - {ont}: {len(freqs):,} unique terms\")\n\n# Save\nwith open(OUTPUT_DIR / 'term_frequencies.pkl', 'wb') as f:\n    pickle.dump(term_frequencies, f)\nwith open(OUTPUT_DIR / 'term_freq_by_ontology.pkl', 'wb') as f:\n    pickle.dump(term_freq_by_ontology, f)\n\nprint(\"\\n✓ Saved: term_frequencies.pkl, term_freq_by_ontology.pkl\")\n\n# ============================================================================\n# 5. PROTEIN LABEL STATISTICS\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"5. PROTEIN LABEL STATISTICS\")\nprint(\"=\" * 80)\n\n# Count terms per protein\nproteins_terms = train_terms_df.groupby('protein_id')['go_term'].count()\nprint(f\"\\nTerms per Protein Statistics:\")\nprint(f\"  - Mean: {proteins_terms.mean():.1f}\")\nprint(f\"  - Median: {proteins_terms.median():.1f}\")\nprint(f\"  - Min: {proteins_terms.min()}\")\nprint(f\"  - Max: {proteins_terms.max()}\")\n\n# Proteins by ontology\nproteins_by_ont = train_terms_df.groupby('ontology')['protein_id'].nunique()\nprint(f\"\\nProteins with annotations by Ontology:\")\nfor ont, count in proteins_by_ont.items():\n    print(f\"  - {ont}: {count:,} proteins\")\n\n# Save protein labels as dict\nprotein_labels = defaultdict(lambda: {'BPO': set(), 'CCO': set(), 'MFO': set()})\nfor _, row in train_terms_df.iterrows():\n    protein_labels[row['protein_id']][row['ontology']].add(row['go_term'])\n\n# Convert sets to lists for pickling\nprotein_labels = {k: {ont: list(v[ont]) for ont in ['BPO', 'CCO', 'MFO']} \n                  for k, v in protein_labels.items()}\n\nwith open(OUTPUT_DIR / 'protein_labels.pkl', 'wb') as f:\n    pickle.dump(protein_labels, f)\n\nprint(\"\\n✓ Saved: protein_labels.pkl\")\n\n# ============================================================================\n# 6. LOAD TEST SUPERSET\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"6. LOADING TEST SUPERSET\")\nprint(\"=\" * 80)\n\n# Parse test sequences\nprint(\"Parsing testsuperset.fasta...\")\ntest_sequences = parse_fasta(TEST_PATH / 'testsuperset.fasta')\nprint(f\"✓ Loaded {len(test_sequences):,} test protein sequences\")\n\n# Test sequence length statistics\ntest_seq_lengths = [len(seq) for seq in test_sequences.values()]\nprint(f\"\\nTest Sequence Length Statistics:\")\nprint(f\"  - Mean: {np.mean(test_seq_lengths):.1f}\")\nprint(f\"  - Median: {np.median(test_seq_lengths):.1f}\")\nprint(f\"  - Min: {np.min(test_seq_lengths)}\")\nprint(f\"  - Max: {np.max(test_seq_lengths)}\")\n\n# Load test taxonomy\nprint(\"\\nLoading testsuperset-taxon-list.tsv...\")\ntest_taxonomy_df = pd.read_csv(TEST_PATH / 'testsuperset-taxon-list.tsv', sep='\\t',\n                                header=None, names=['taxon_id'])\nprint(f\"✓ Test taxon species: {len(test_taxonomy_df):,}\")\n\n# Save\nwith open(OUTPUT_DIR / 'test_sequences.pkl', 'wb') as f:\n    pickle.dump(test_sequences, f)\n\nprint(\"\\n✓ Saved: test_sequences.pkl\")\n\n# ============================================================================\n# 7. CREATE DATA STATISTICS SUMMARY\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"7. CREATING DATA STATISTICS SUMMARY\")\nprint(\"=\" * 80)\n\ndata_stats = {\n    # GO Graph\n    'go_graph_nodes': go_graph.number_of_nodes(),\n    'go_graph_edges': go_graph.number_of_edges(),\n    'ontology_term_counts': {ont: len(terms) for ont, terms in ontology_terms.items()},\n    \n    # IA Weights\n    'ia_terms_count': len(ia_weights),\n    'ia_weight_stats': {\n        'mean': float(ia_df['ia_weight'].mean()),\n        'median': float(ia_df['ia_weight'].median()),\n        'min': float(ia_df['ia_weight'].min()),\n        'max': float(ia_df['ia_weight'].max())\n    },\n    \n    # Training Data\n    'train_proteins': len(train_sequences),\n    'train_associations': len(train_terms_df),\n    'train_unique_terms': len(term_frequencies),\n    'train_species': int(train_taxonomy_df['taxon_id'].nunique()),\n    'train_seq_length_mean': float(np.mean(seq_lengths)),\n    'train_seq_length_median': float(np.median(seq_lengths)),\n    'train_seq_length_max': int(np.max(seq_lengths)),\n    \n    # Term frequencies\n    'terms_per_protein_mean': float(proteins_terms.mean()),\n    'terms_per_protein_median': float(proteins_terms.median()),\n    'terms_per_protein_max': int(proteins_terms.max()),\n    \n    # Test Data\n    'test_proteins': len(test_sequences),\n    'test_species': len(test_taxonomy_df),\n    'test_seq_length_mean': float(np.mean(test_seq_lengths)),\n    'test_seq_length_median': float(np.median(test_seq_lengths)),\n    'test_seq_length_max': int(np.max(test_seq_lengths)),\n    \n    # By Ontology\n    'train_proteins_by_ontology': proteins_by_ont.to_dict(),\n    'train_terms_by_ontology': {ont: len(freqs) for ont, freqs in term_freq_by_ontology.items()}\n}\n\n# Save\nwith open(OUTPUT_DIR / 'data_stats.pkl', 'wb') as f:\n    pickle.dump(data_stats, f)\n\nprint(\"\\n✓ Saved: data_stats.pkl\")\n\n# Print summary\nprint(\"\\n\" + \"=\" * 80)\nprint(\"DATA STATISTICS SUMMARY\")\nprint(\"=\" * 80)\nprint(f\"\\n📊 GO Ontology:\")\nprint(f\"  - Nodes: {data_stats['go_graph_nodes']:,}\")\nprint(f\"  - Edges: {data_stats['go_graph_edges']:,}\")\nprint(f\"  - BPO terms: {data_stats['ontology_term_counts']['BPO']:,}\")\nprint(f\"  - CCO terms: {data_stats['ontology_term_counts']['CCO']:,}\")\nprint(f\"  - MFO terms: {data_stats['ontology_term_counts']['MFO']:,}\")\n\nprint(f\"\\n📊 Training Data:\")\nprint(f\"  - Proteins: {data_stats['train_proteins']:,}\")\nprint(f\"  - Associations: {data_stats['train_associations']:,}\")\nprint(f\"  - Unique GO terms: {data_stats['train_unique_terms']:,}\")\nprint(f\"  - Species: {data_stats['train_species']:,}\")\nprint(f\"  - Avg sequence length: {data_stats['train_seq_length_mean']:.1f}\")\n\nprint(f\"\\n📊 Test Data:\")\nprint(f\"  - Proteins: {data_stats['test_proteins']:,}\")\nprint(f\"  - Species: {data_stats['test_species']:,}\")\nprint(f\"  - Avg sequence length: {data_stats['test_seq_length_mean']:.1f}\")\n\n# ============================================================================\n# 8. COMPUTE GO GRAPH PROPERTIES\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"8. COMPUTING GO GRAPH PROPERTIES\")\nprint(\"=\" * 80)\n\n# Compute depths for each term (distance from root)\nterm_depths = {}\n\nfor ont_name, root_term in subontology_roots.items():\n    if root_term in go_graph:\n        # BFS to compute depths\n        depths = {root_term: 0}\n        queue = [root_term]\n        \n        while queue:\n            current = queue.pop(0)\n            current_depth = depths[current]\n            \n            # Get children (predecessors in our directed graph)\n            for child in go_graph.predecessors(current):\n                if child not in depths:\n                    depths[child] = current_depth + 1\n                    queue.append(child)\n        \n        term_depths.update(depths)\n        print(f\"✓ {ont_name}: Computed depths for {len(depths):,} terms\")\n\n# Save\nwith open(OUTPUT_DIR / 'term_depths.pkl', 'wb') as f:\n    pickle.dump(term_depths, f)\n\nprint(\"\\n✓ Saved: term_depths.pkl\")\n\n# Depth statistics\ndepth_values = list(term_depths.values())\nprint(f\"\\nTerm Depth Statistics:\")\nprint(f\"  - Mean: {np.mean(depth_values):.2f}\")\nprint(f\"  - Median: {np.median(depth_values):.2f}\")\nprint(f\"  - Max: {np.max(depth_values)}\")\n\n# ============================================================================\n# 9. CREATE TERM ANCESTORS/DESCENDANTS MAPPING\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"9. CREATING ANCESTOR/DESCENDANT MAPPINGS\")\nprint(\"=\" * 80)\n\n# For each term, find all ancestors and descendants\nterm_ancestors = {}\nterm_descendants = {}\n\nfor term in tqdm(go_graph.nodes(), desc=\"Computing relationships\"):\n    # Ancestors (all nodes reachable going UP the hierarchy)\n    ancestors = set(nx.descendants(go_graph, term))  # descendants in DiGraph = ancestors in GO\n    term_ancestors[term] = list(ancestors)\n    \n    # Descendants (all nodes reachable going DOWN the hierarchy)\n    descendants = set(nx.ancestors(go_graph, term))  # ancestors in DiGraph = descendants in GO\n    term_descendants[term] = list(descendants)\n\nprint(f\"\\n✓ Computed ancestors/descendants for {len(term_ancestors):,} terms\")\n\n# Save\nwith open(OUTPUT_DIR / 'term_ancestors.pkl', 'wb') as f:\n    pickle.dump(term_ancestors, f)\nwith open(OUTPUT_DIR / 'term_descendants.pkl', 'wb') as f:\n    pickle.dump(term_descendants, f)\n\nprint(\"✓ Saved: term_ancestors.pkl, term_descendants.pkl\")\n\n# ============================================================================\n# 10. FINAL SUMMARY\n# ============================================================================\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"NOTEBOOK 1 COMPLETED SUCCESSFULLY! ✅\")\nprint(\"=\" * 80)\n\nprint(\"\\n📦 Generated Files:\")\noutput_files = [\n    'go_graph.pkl',\n    'term_info.pkl',\n    'ontology_terms.pkl',\n    'ia_weights.pkl',\n    'train_sequences.pkl',\n    'test_sequences.pkl',\n    'term_frequencies.pkl',\n    'term_freq_by_ontology.pkl',\n    'protein_labels.pkl',\n    'data_stats.pkl',\n    'term_depths.pkl',\n    'term_ancestors.pkl',\n    'term_descendants.pkl'\n]\n\nfor i, fname in enumerate(output_files, 1):\n    fpath = OUTPUT_DIR / fname\n    size_mb = fpath.stat().st_size / (1024**2)\n    print(f\"  {i:2d}. {fname:<35s} ({size_mb:6.2f} MB)\")\n\nprint(f\"\\n✅ Total files generated: {len(output_files)}\")\nprint(f\"✅ Ready for Notebook 2: Feature Engineering\")\n\nprint(\"\\n\" + \"=\" * 80)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-23T12:03:19.878902Z","iopub.execute_input":"2026-01-23T12:03:19.879243Z","iopub.status.idle":"2026-01-23T12:04:15.780739Z","shell.execute_reply.started":"2026-01-23T12:03:19.879213Z","shell.execute_reply":"2026-01-23T12:04:15.779790Z"}},"outputs":[],"execution_count":null}]}