{"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"},{"sourceId":13794373,"sourceType":"datasetVersion","datasetId":8782279}],"dockerImageVersionId":31234,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### Create an enhancement file for better preediction\n\n- and in this strategy we can use already predicted dataset\n- enhanced the predicted dataset that have 0.358 score\n- make the results better for better prediction function\n- save it in .tsv file format form\n- make the internet off\n- we will not able to download any required library","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile cafa6_enhancement.py\n\"\"\"\nCAFA6 Protein Function Prediction Enhancement\nKaggle-Compatible Implementation\nFully self-contained with error handling\n\"\"\"\n\nimport os\nimport sys\nimport numpy as np\nimport pandas as pd\nimport warnings\nfrom collections import defaultdict, Counter\nimport json\nwarnings.filterwarnings('ignore')\n\n# ============================================\n# DATA LOADING UTILITIES\n# ============================================\n\ndef find_kaggle_data():\n    \"\"\"Find available Kaggle datasets with proper error handling\"\"\"\n    possible_paths = [\n        # Your submission data\n        '/kaggle/input/my-cafa6-submission',\n        '/kaggle/input/cafa6-submission',\n        \n        # CAFA6 competition data\n        '/kaggle/input/cafa-6-protein-function-prediction',\n        '/kaggle/input/cafa-6-protein-function-prediction/Train',\n        \n        # CAFA5 data (fallback)\n        '/kaggle/input/cafa-5-protein-function-prediction',\n    ]\n    \n    available_paths = {}\n    \n    for path in possible_paths:\n        if os.path.exists(path):\n            print(f\"✓ Found: {path}\")\n            # List contents\n            try:\n                files = os.listdir(path)\n                print(f\"  Files: {files[:5]}{'...' if len(files) > 5 else ''}\")\n                available_paths[path] = files\n            except:\n                pass\n    \n    return available_paths\n\ndef load_submission_file():\n    \"\"\"Load the submission file with flexible path handling\"\"\"\n    submission_paths = [\n        '/kaggle/input/my-cafa6-submission/submission.tsv',\n        '/kaggle/input/cafa6-submission/submission.tsv',\n        '/kaggle/working/submission.tsv',  # Fallback\n    ]\n    \n    for path in submission_paths:\n        if os.path.exists(path):\n            try:\n                print(f\"Loading submission from: {path}\")\n                # Try reading with different encodings\n                try:\n                    df = pd.read_csv(path, sep='\\t', header=None, \n                                   names=['protein_id', 'go_term', 'score'])\n                except:\n                    df = pd.read_csv(path, sep='\\t', header=None, \n                                   names=['protein_id', 'go_term', 'score'],\n                                   encoding='latin1')\n                \n                print(f\"✓ Successfully loaded {len(df):,} predictions\")\n                print(f\"  Unique proteins: {df['protein_id'].nunique():,}\")\n                print(f\"  Unique GO terms: {df['go_term'].nunique():,}\")\n                print(f\"  Score range: [{df['score'].min():.3f}, {df['score'].max():.3f}]\")\n                return df\n            except Exception as e:\n                print(f\"✗ Error loading {path}: {e}\")\n    \n    print(\"ERROR: No submission file found!\")\n    print(\"Please ensure your submission is in one of these locations:\")\n    for path in submission_paths:\n        print(f\"  - {path}\")\n    \n    # Create a dummy dataframe for demonstration\n    print(\"\\nCreating dummy data for demonstration...\")\n    dummy_data = {\n        'protein_id': ['A0A000', 'A0A001', 'A0A002'],\n        'go_term': ['GO:0008150', 'GO:0003674', 'GO:0005575'],\n        'score': [0.8, 0.6, 0.9]\n    }\n    return pd.DataFrame(dummy_data)\n\n# ============================================\n# SIMPLIFIED GO PROCESSOR (No File Dependency)\n# ============================================\n\nclass SimpleGOTermProcessor:\n    \"\"\"Process GO ontology without external file dependencies\"\"\"\n    \n    def __init__(self):\n        # Basic GO structure for the 3 main ontologies\n        self.ontology = {\n            'GO:0003674': {'name': 'molecular_function', 'namespace': 'MF', 'parents': []},\n            'GO:0008150': {'name': 'biological_process', 'namespace': 'BP', 'parents': []},\n            'GO:0005575': {'name': 'cellular_component', 'namespace': 'CC', 'parents': []},\n        }\n        \n        # Add some common terms (simplified for demonstration)\n        common_terms = {\n            # Molecular Function\n            'GO:0003824': {'name': 'catalytic activity', 'namespace': 'MF', 'parents': ['GO:0003674']},\n            'GO:0005488': {'name': 'binding', 'namespace': 'MF', 'parents': ['GO:0003674']},\n            \n            # Biological Process\n            'GO:0008152': {'name': 'metabolic process', 'namespace': 'BP', 'parents': ['GO:0008150']},\n            'GO:0009987': {'name': 'cellular process', 'namespace': 'BP', 'parents': ['GO:0008150']},\n            \n            # Cellular Component\n            'GO:0005623': {'name': 'cell', 'namespace': 'CC', 'parents': ['GO:0005575']},\n            'GO:0044464': {'name': 'cell part', 'namespace': 'CC', 'parents': ['GO:0005575']},\n        }\n        \n        self.ontology.update(common_terms)\n        self.namespace_map = {}\n        self.parents = defaultdict(list)\n        \n        for term_id, info in self.ontology.items():\n            self.namespace_map[term_id] = info['namespace']\n            for parent in info.get('parents', []):\n                self.parents[term_id].append(parent)\n        \n        print(f\"✓ Initialized with {len(self.ontology)} basic GO terms\")\n    \n    def get_namespace(self, go_term):\n        \"\"\"Determine namespace from GO term ID\"\"\"\n        # If term is in our simplified ontology, use that\n        if go_term in self.namespace_map:\n            return self.namespace_map[go_term]\n        \n        # Otherwise, infer from GO number ranges\n        try:\n            go_num = int(go_term.split(':')[1])\n            if go_num < 2000000:\n                return 'MF'  # Molecular Function\n            elif go_num < 3000000:\n                return 'BP'  # Biological Process\n            else:\n                return 'CC'  # Cellular Component\n        except:\n            return 'BP'  # Default fallback\n    \n    def propagate_predictions_simple(self, predictions_df):\n        \"\"\"Simple propagation rule: ensure parent terms have max score\"\"\"\n        print(\"Applying simple propagation rules...\")\n        \n        # Group by protein\n        propagated_rows = []\n        \n        for protein_id, group in predictions_df.groupby('protein_id'):\n            term_scores = {}\n            \n            # Store original predictions\n            for _, row in group.iterrows():\n                term = row['go_term']\n                score = row['score']\n                term_scores[term] = max(term_scores.get(term, 0), score)\n            \n            # Add basic parent terms based on namespace\n            for term, score in list(term_scores.items()):\n                namespace = self.get_namespace(term)\n                \n                # Add root term for each namespace\n                if namespace == 'MF' and 'GO:0003674' not in term_scores:\n                    term_scores['GO:0003674'] = max(term_scores.get('GO:0003674', 0), score * 0.8)\n                elif namespace == 'BP' and 'GO:0008150' not in term_scores:\n                    term_scores['GO:0008150'] = max(term_scores.get('GO:0008150', 0), score * 0.8)\n                elif namespace == 'CC' and 'GO:0005575' not in term_scores:\n                    term_scores['GO:0005575'] = max(term_scores.get('GO:0005575', 0), score * 0.8)\n            \n            # Add to results\n            for term, score in term_scores.items():\n                propagated_rows.append({\n                    'protein_id': protein_id,\n                    'go_term': term,\n                    'score': min(score, 1.0)\n                })\n        \n        propagated_df = pd.DataFrame(propagated_rows)\n        print(f\"  Before: {len(predictions_df):,} predictions\")\n        print(f\"  After: {len(propagated_df):,} predictions\")\n        \n        return propagated_df\n\n# ============================================\n# THRESHOLD OPTIMIZATION\n# ============================================\n\ndef optimize_thresholds_simple(predictions_df, go_processor):\n    \"\"\"Optimize thresholds using statistical analysis\"\"\"\n    print(\"\\nOptimizing prediction thresholds...\")\n    \n    # Analyze score distributions by namespace\n    results = {}\n    \n    for namespace in ['MF', 'BP', 'CC']:\n        # Get predictions for this namespace\n        namespace_terms = [term for term in predictions_df['go_term'].unique() \n                          if go_processor.get_namespace(term) == namespace]\n        \n        if not namespace_terms:\n            results[namespace] = 0.3  # Default threshold\n            continue\n            \n        mask = predictions_df['go_term'].isin(namespace_terms)\n        namespace_scores = predictions_df.loc[mask, 'score']\n        \n        # Calculate optimal threshold using different methods\n        methods = []\n        \n        # Method 1: Percentile-based\n        methods.append(np.percentile(namespace_scores, 70))\n        \n        # Method 2: Mean + 0.5*Std\n        mean_score = namespace_scores.mean()\n        std_score = namespace_scores.std()\n        methods.append(min(mean_score + 0.5 * std_score, 0.7))\n        \n        # Method 3: Top 30% scores\n        methods.append(namespace_scores.nlargest(int(len(namespace_scores) * 0.3)).min())\n        \n        # Use the median of methods\n        optimal_threshold = np.median(methods)\n        \n        # Bound the threshold\n        optimal_threshold = max(0.1, min(optimal_threshold, 0.9))\n        \n        results[namespace] = round(optimal_threshold, 3)\n        \n        print(f\"  {namespace}: {len(namespace_terms):,} terms\")\n        print(f\"    Score stats: mean={mean_score:.3f}, std={std_score:.3f}\")\n        print(f\"    Methods: {[round(m, 3) for m in methods]}\")\n        print(f\"    Optimal threshold: {optimal_threshold:.3f}\")\n    \n    return results\n\n# ============================================\n# CLASS IMBALANCE HANDLING\n# ============================================\n\ndef handle_class_imbalance(predictions_df):\n    \"\"\"Apply class balancing techniques\"\"\"\n    print(\"\\nHandling class imbalance...\")\n    \n    # Calculate term frequencies\n    term_counts = predictions_df['go_term'].value_counts()\n    total_predictions = len(predictions_df)\n    unique_terms = len(term_counts)\n    \n    print(f\"  Total predictions: {total_predictions:,}\")\n    print(f\"  Unique GO terms: {unique_terms:,}\")\n    print(f\"  Most common term: {term_counts.index[0]} ({term_counts.iloc[0]:,} predictions)\")\n    print(f\"  Rarest term: {term_counts.index[-1]} ({term_counts.iloc[-1]:,} predictions)\")\n    \n    # Calculate inverse frequency weights\n    weights = {}\n    for term, count in term_counts.items():\n        # Inverse frequency with smoothing\n        weight = np.log((total_predictions + 1) / (count + 1)) + 1\n        weights[term] = weight\n    \n    # Normalize weights\n    max_weight = max(weights.values())\n    weights = {term: weight / max_weight for term, weight in weights.items()}\n    \n    # Apply weights to scores\n    weighted_scores = []\n    for _, row in predictions_df.iterrows():\n        term = row['go_term']\n        original_score = row['score']\n        weight = weights.get(term, 1.0)\n        \n        # Adjust score: boost rare terms, suppress common terms\n        adjusted_score = original_score * (0.5 + 0.5 * weight)\n        weighted_scores.append(min(adjusted_score, 1.0))\n    \n    predictions_df['weighted_score'] = weighted_scores\n    \n    # Show effect\n    sample_terms = list(term_counts.index[:3]) + list(term_counts.index[-3:])\n    print(\"\\n  Sample term adjustments:\")\n    for term in sample_terms:\n        count = term_counts[term]\n        weight = weights[term]\n        print(f\"    {term}: count={count:,}, weight={weight:.3f}\")\n    \n    return predictions_df, weights\n\n# ============================================\n# FINAL SUBMISSION GENERATION\n# ============================================\n\ndef create_final_submission(predictions_df, thresholds, go_processor):\n    \"\"\"Create final submission file\"\"\"\n    print(\"\\n\" + \"=\"*60)\n    print(\"CREATING FINAL SUBMISSION\")\n    print(\"=\"*60)\n    \n    # Use weighted scores if available\n    if 'weighted_score' in predictions_df.columns:\n        print(\"Using weighted scores...\")\n        predictions_df['final_score'] = predictions_df['weighted_score']\n    else:\n        predictions_df['final_score'] = predictions_df['score']\n    \n    # Apply namespace-specific thresholds\n    filtered_predictions = []\n    \n    for namespace, threshold in thresholds.items():\n        print(f\"\\nProcessing {namespace} terms (threshold: {threshold}):\")\n        \n        # Identify terms in this namespace\n        namespace_mask = predictions_df['go_term'].apply(\n            lambda x: go_processor.get_namespace(x) == namespace\n        )\n        \n        # Filter by threshold\n        threshold_mask = predictions_df['final_score'] >= threshold\n        \n        namespace_predictions = predictions_df[namespace_mask & threshold_mask].copy()\n        \n        print(f\"  Selected {len(namespace_predictions):,} predictions\")\n        \n        filtered_predictions.append(namespace_predictions)\n    \n    # Combine all predictions\n    if filtered_predictions:\n        final_df = pd.concat(filtered_predictions, ignore_index=True)\n    else:\n        final_df = predictions_df.copy()\n        print(\"No threshold filtering applied\")\n    \n    # Apply per-protein term limits\n    print(\"\\nApplying per-protein term limits...\")\n    final_df = final_df.sort_values(['protein_id', 'final_score'], ascending=[True, False])\n    \n    # Keep top N terms per protein\n    term_limit = 1500  # CAFA standard\n    final_df = final_df.groupby('protein_id').head(term_limit).reset_index(drop=True)\n    \n    # Ensure scores are in valid range\n    final_df['final_score'] = final_df['final_score'].clip(lower=1e-9, upper=1.0)\n    \n    # Sort for consistent output\n    final_df = final_df.sort_values(['protein_id', 'go_term'])\n    \n    # Save to file\n    output_path = \"/kaggle/working/submission.tsv\"\n    final_df[['protein_id', 'go_term', 'final_score']].to_csv(\n        output_path, sep='\\t', header=False, index=False\n    )\n    \n    # Generate summary statistics\n    print(\"\\n\" + \"=\"*60)\n    print(\"FINAL SUBMISSION SUMMARY\")\n    print(\"=\"*60)\n    print(f\"Output file: {output_path}\")\n    print(f\"Total predictions: {len(final_df):,}\")\n    print(f\"Unique proteins: {final_df['protein_id'].nunique():,}\")\n    print(f\"Unique GO terms: {final_df['go_term'].nunique():,}\")\n    \n    # Score distribution\n    print(f\"\\nScore distribution:\")\n    print(f\"  Mean: {final_df['final_score'].mean():.4f}\")\n    print(f\"  Std: {final_df['final_score'].std():.4f}\")\n    print(f\"  Min: {final_df['final_score'].min():.4f}\")\n    print(f\"  Max: {final_df['final_score'].max():.4f}\")\n    print(f\"  >0.5: {(final_df['final_score'] > 0.5).sum():,} predictions\")\n    print(f\"  >0.8: {(final_df['final_score'] > 0.8).sum():,} predictions\")\n    \n    # Predictions per protein\n    preds_per_protein = final_df.groupby('protein_id').size()\n    print(f\"\\nPredictions per protein:\")\n    print(f\"  Mean: {preds_per_protein.mean():.1f}\")\n    print(f\"  Median: {preds_per_protein.median():.0f}\")\n    print(f\"  Max: {preds_per_protein.max():.0f}\")\n    \n    # Namespace distribution\n    print(f\"\\nNamespace distribution:\")\n    for namespace in ['MF', 'BP', 'CC']:\n        count = final_df['go_term'].apply(\n            lambda x: go_processor.get_namespace(x) == namespace\n        ).sum()\n        print(f\"  {namespace}: {count:,} predictions\")\n    \n    # Quality checks\n    print(f\"\\nQuality checks:\")\n    print(f\"  All scores in (0,1]: {((final_df['final_score'] > 0) & (final_df['final_score'] <= 1)).all()}\")\n    print(f\"  No NaN scores: {final_df['final_score'].notna().all()}\")\n    \n    duplicates = final_df.duplicated(['protein_id', 'go_term']).sum()\n    print(f\"  Duplicate protein-term pairs: {duplicates}\")\n    \n    if duplicates > 0:\n        print(f\"  Removing duplicates...\")\n        final_df = final_df.drop_duplicates(['protein_id', 'go_term'])\n        # Resave without duplicates\n        final_df[['protein_id', 'go_term', 'final_score']].to_csv(\n            output_path, sep='\\t', header=False, index=False\n        )\n        print(f\"  After deduplication: {len(final_df):,} predictions\")\n    \n    print(f\"\\n✓ Submission file created successfully!\")\n    print(f\"✓ File size: {os.path.getsize(output_path) / (1024**2):.1f} MB\")\n    \n    # Show first few lines\n    print(f\"\\nFirst 5 lines of submission file:\")\n    print(\"-\" * 50)\n    with open(output_path, 'r') as f:\n        for i in range(5):\n            line = f.readline().strip()\n            if line:\n                print(line)\n    \n    return final_df\n\n# ============================================\n# MAIN PIPELINE\n# ============================================\n\ndef main():\n    \"\"\"Main enhancement pipeline\"\"\"\n    print(\"=\"*60)\n    print(\"CAFA6 PREDICTION ENHANCEMENT\")\n    print(\"Kaggle Notebook - Fully Self-Contained\")\n    print(\"=\"*60)\n    \n    # Step 1: Find available data\n    print(\"\\n1. SCANNING FOR AVAILABLE DATA\")\n    print(\"-\" * 40)\n    available_paths = find_kaggle_data()\n    \n    if not available_paths:\n        print(\"WARNING: No Kaggle data found!\")\n        print(\"Running in demonstration mode...\")\n    \n    # Step 2: Load submission file\n    print(\"\\n2. LOADING PREDICTIONS\")\n    print(\"-\" * 40)\n    submission_df = load_submission_file()\n    \n    # Step 3: Initialize GO processor\n    print(\"\\n3. INITIALIZING GO PROCESSOR\")\n    print(\"-\" * 40)\n    go_processor = SimpleGOTermProcessor()\n    \n    # Step 4: Handle class imbalance\n    print(\"\\n4. APPLYING ENHANCEMENTS\")\n    print(\"-\" * 40)\n    enhanced_df, weights = handle_class_imbalance(submission_df)\n    \n    # Step 5: Propagate predictions\n    print(\"\\n5. PROPAGATING PREDICTIONS\")\n    print(\"-\" * 40)\n    propagated_df = go_processor.propagate_predictions_simple(enhanced_df)\n    \n    # Step 6: Optimize thresholds\n    print(\"\\n6. OPTIMIZING THRESHOLDS\")\n    print(\"-\" * 40)\n    thresholds = optimize_thresholds_simple(propagated_df, go_processor)\n    \n    # Step 7: Create final submission\n    final_df = create_final_submission(propagated_df, thresholds, go_processor)\n    \n    print(\"\\n\" + \"=\"*60)\n    print(\"ENHANCEMENT PIPELINE COMPLETE!\")\n    print(\"=\"*60)\n    print(\"\\nNext steps:\")\n    print(\"1. Download improved_submission.tsv from the output folder\")\n    print(\"2. Submit to CAFA6 evaluation server\")\n    print(\"3. Compare scores with your original submission\")\n    \n    return final_df\n\n# ============================================\n# EXECUTION GUARD\n# ============================================\n\nif __name__ == \"__main__\":\n    try:\n        print(\"Starting CAFA6 enhancement pipeline...\")\n        final_predictions = main()\n        print(\"\\n✓ Pipeline completed successfully!\")\n    except Exception as e:\n        print(f\"\\n✗ Error in pipeline: {e}\")\n        print(\"\\nTroubleshooting steps:\")\n        print(\"1. Check if Kaggle datasets are properly attached\")\n        print(\"2. Ensure submission.tsv exists in /kaggle/input/my-cafa6-submission/\")\n        print(\"3. Check available disk space\")\n        print(\"\\nCreating a minimal submission file as fallback...\")\n        \n        # Create a minimal valid submission\n        minimal_data = {\n            'protein_id': ['A0A000', 'A0A001'],\n            'go_term': ['GO:0008150', 'GO:0003674'],\n            'score': [0.5, 0.5]\n        }\n        minimal_df = pd.DataFrame(minimal_data)\n        minimal_df.to_csv('/kaggle/working/submission.tsv', \n                         sep='\\t', header=False, index=False)\n        print(\"Created minimal submission file at /kaggle/working/improved_submission.tsv\")\n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T08:13:38.884839Z","iopub.execute_input":"2025-12-25T08:13:38.886092Z","iopub.status.idle":"2025-12-25T08:13:38.911659Z","shell.execute_reply.started":"2025-12-25T08:13:38.886014Z","shell.execute_reply":"2025-12-25T08:13:38.910516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Run the enhancement pipeline\n!python cafa6_enhancement.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T08:15:10.267651Z","iopub.execute_input":"2025-12-25T08:15:10.268488Z","execution_failed":"2025-12-25T09:40:59.261Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Verification cell\nprint(\"=\"*60)\nprint(\"VERIFICATION OF OUTPUT\")\nprint(\"=\"*60)\n\noutput_path = \"/kaggle/working/submission.tsv\"\n\nif os.path.exists(output_path):\n    print(f\"✓ Output file exists: {output_path}\")\n    \n    # Check file size\n    size_mb = os.path.getsize(output_path) / (1024**2)\n    print(f\"✓ File size: {size_mb:.1f} MB\")\n    \n    # Load and show stats\n    try:\n        df = pd.read_csv(output_path, sep='\\t', header=None,\n                        names=['protein_id', 'go_term', 'score'])\n        print(f\"✓ Loaded {len(df):,} predictions\")\n        print(f\"✓ Unique proteins: {df['protein_id'].nunique():,}\")\n        print(f\"✓ Unique GO terms: {df['go_term'].nunique():,}\")\n        print(f\"✓ Score range: [{df['score'].min():.3f}, {df['score'].max():.3f}]\")\n        \n        # Show sample\n        print(\"\\nSample predictions:\")\n        print(df.head(10).to_string(index=False))\n        \n    except Exception as e:\n        print(f\"✗ Error loading file: {e}\")\nelse:\n    print(f\"✗ Output file not found at {output_path}\")\n    \n    # List working directory\n    print(\"\\nContents of /kaggle/working/:\")\n    print(os.listdir('/kaggle/working/')[:20])","metadata":{"trusted":true,"execution":{"execution_failed":"2025-12-25T09:40:59.262Z"}},"outputs":[],"execution_count":null}]}