{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"},{"sourceId":11060723,"sourceType":"datasetVersion","datasetId":6891568},{"sourceId":392596,"sourceType":"modelInstanceVersion","modelInstanceId":316555,"modelId":337067}],"dockerImageVersionId":30918,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# **BirdCLEF 2025 Inference Notebook**\nThis notebook runs inference on BirdCLEF 2025 test soundscapes and generates a submission file. It supports both single model inference and ensemble inference with multiple models. You can find the pre-processing and training processes in the following notebooks:\n\n- [Transforming Audio-to-Mel Spec. | BirdCLEF'25](https://www.kaggle.com/code/kadircandrisolu/transforming-audio-to-mel-spec-birdclef-25)  \n- [EfficientNet B0 Pytorch [Train] | BirdCLEF'25](https://www.kaggle.com/code/kadircandrisolu/efficientnet-b0-pytorch-train-birdclef-25)\n\n**Features**\n- Audio Preprocessing\n- Test-Time Augmentation (TTA)","metadata":{}},{"cell_type":"code","source":"import os\nimport gc\nimport warnings\nimport logging\nimport time\nimport math\nimport cv2\nfrom pathlib import Path\nimport plotly.graph_objects as go\nimport plotly.express as px\nimport IPython.display as ipd\nfrom scipy.signal import butter, filtfilt\n\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport seaborn as sns\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport timm\nfrom tqdm.auto import tqdm\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import confusion_matrix\n\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T18:04:15.128807Z","iopub.execute_input":"2025-05-17T18:04:15.129084Z","iopub.status.idle":"2025-05-17T18:04:32.574003Z","shell.execute_reply.started":"2025-05-17T18:04:15.129064Z","shell.execute_reply":"2025-05-17T18:04:32.573074Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CFG:\n    seed = 42\n    n_fold = 5\n \n    test_soundscapes = '/kaggle/input/birdclef-2025/train_soundscapes'\n    submission_csv = '/kaggle/input/birdclef-2025/sample_submission.csv'\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n    train_csv = '/kaggle/input/birdclef-2025/train.csv'\n    train_path = '/kaggle/input/birdclef-2025/train_audio'\n    model_path = '/kaggle/input/efficientnet-team-rocket/pytorch/default/2'\n    # model_path = '/kaggle/input/birdclef25-effnetb0-starter-weight'  \n    \n    # Audio parameters\n    FS = 32000  \n    WINDOW_SIZE = 5  \n    \n    # Mel spectrogram parameters\n    N_FFT = 1024\n    HOP_LENGTH = 512\n    N_MELS = 128\n    FMIN = 50\n    FMAX = 14000\n    TARGET_SHAPE = (256, 256)\n    \n    model_name = 'efficientnet_b0'\n    in_channels = 1\n    device = 'cuda'  \n    \n    # Inference parameters\n    batch_size = 128\n    use_tta = False  \n    tta_count = 3   \n    threshold = 0.5\n    \n    use_specific_folds = True  # If False, use all found models\n    folds = [0]  # Used only if use_specific_folds is True\n    \n    debug = False\n    debug_count = 200\n\ncfg = CFG()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T18:04:37.116693Z","iopub.execute_input":"2025-05-17T18:04:37.117009Z","iopub.status.idle":"2025-05-17T18:04:37.122413Z","shell.execute_reply.started":"2025-05-17T18:04:37.116981Z","shell.execute_reply":"2025-05-17T18:04:37.121724Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Using device: {cfg.device}\")\nprint(f\"Loading taxonomy data...\")\ntaxonomy_df = pd.read_csv(cfg.taxonomy_csv)\nspecies_ids = taxonomy_df['primary_label'].tolist()\nnum_classes = len(species_ids)\nlabel_to_idx = {label: idx for idx, label in enumerate(species_ids)}\nprint(f\"Number of classes: {num_classes}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T18:04:41.026501Z","iopub.execute_input":"2025-05-17T18:04:41.026819Z","iopub.status.idle":"2025-05-17T18:04:41.054834Z","shell.execute_reply.started":"2025-05-17T18:04:41.026791Z","shell.execute_reply":"2025-05-17T18:04:41.054134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdCLEFModel(nn.Module):\n    def __init__(self, cfg, num_classes):\n        super().__init__()\n        self.cfg = cfg\n        \n        self.backbone = timm.create_model(\n            cfg.model_name,\n            pretrained=False,  \n            in_chans=cfg.in_channels,\n            drop_rate=0.0,    \n            drop_path_rate=0.0\n        )\n        \n        if 'efficientnet' in cfg.model_name:\n            backbone_out = self.backbone.classifier.in_features\n            self.backbone.classifier = nn.Identity()\n        elif 'resnet' in cfg.model_name:\n            backbone_out = self.backbone.fc.in_features\n            self.backbone.fc = nn.Identity()\n        else:\n            backbone_out = self.backbone.get_classifier().in_features\n            self.backbone.reset_classifier(0, '')\n        \n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        self.feat_dim = backbone_out\n        self.classifier = nn.Linear(backbone_out, num_classes)\n        \n    def forward(self, x):\n        features = self.backbone(x)\n        \n        if isinstance(features, dict):\n            features = features['features']\n            \n        if len(features.shape) == 4:\n            features = self.pooling(features)\n            features = features.view(features.size(0), -1)\n        \n        logits = self.classifier(features)\n        return logits\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T13:56:25.846294Z","iopub.execute_input":"2025-05-17T13:56:25.846555Z","iopub.status.idle":"2025-05-17T13:56:25.852914Z","shell.execute_reply.started":"2025-05-17T13:56:25.846531Z","shell.execute_reply":"2025-05-17T13:56:25.852042Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def audio2melspec(audio_data, cfg):\n    \"\"\"Convert audio data to mel spectrogram\"\"\"\n    if np.isnan(audio_data).any():\n        mean_signal = np.nanmean(audio_data)\n        audio_data = np.nan_to_num(audio_data, nan=mean_signal)\n\n    mel_spec = librosa.feature.melspectrogram(\n        y=audio_data,\n        sr=cfg.FS,\n        n_fft=cfg.N_FFT,\n        hop_length=cfg.HOP_LENGTH,\n        n_mels=cfg.N_MELS,\n        fmin=cfg.FMIN,\n        fmax=cfg.FMAX,\n        power=2.0\n    )\n\n    mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n    mel_spec_norm = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-8)\n    \n    return mel_spec_norm\n\ndef process_audio_segment(audio_data, cfg):\n    \"\"\"Process audio segment to get mel spectrogram\"\"\"\n    if len(audio_data) < cfg.FS * cfg.WINDOW_SIZE:\n        audio_data = np.pad(audio_data, \n                          (0, cfg.FS * cfg.WINDOW_SIZE - len(audio_data)), \n                          mode='constant')\n    \n    mel_spec = audio2melspec(audio_data, cfg)\n    \n    # Resize if needed\n    if mel_spec.shape != cfg.TARGET_SHAPE:\n        mel_spec = cv2.resize(mel_spec, cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n        \n    return mel_spec.astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T13:56:25.853653Z","iopub.execute_input":"2025-05-17T13:56:25.853845Z","iopub.status.idle":"2025-05-17T13:56:25.869650Z","shell.execute_reply.started":"2025-05-17T13:56:25.853828Z","shell.execute_reply":"2025-05-17T13:56:25.869026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def find_model_files(cfg):\n    \"\"\"\n    Find all .pth model files in the specified model directory\n    \"\"\"\n    model_files = []\n    \n    model_dir = Path(cfg.model_path)\n    \n    for path in model_dir.glob('**/*.pth'):\n        model_files.append(str(path))\n    \n    return model_files\n\ndef load_models(cfg, num_classes):\n    \"\"\"\n    Load all found model files and prepare them for ensemble\n    \"\"\"\n    models = []\n    \n    model_files = find_model_files(cfg)\n    \n    if not model_files:\n        print(f\"Warning: No model files found under {cfg.model_path}!\")\n        return models\n    \n    print(f\"Found a total of {len(model_files)} model files.\")\n    \n    if cfg.use_specific_folds:\n        filtered_files = []\n        for fold in cfg.folds:\n            fold_files = [f for f in model_files if f\"fold{fold}\" in f]\n            filtered_files.extend(fold_files)\n        model_files = filtered_files\n        print(f\"Using {len(model_files)} model files for the specified folds ({cfg.folds}).\")\n    \n    for model_path in model_files:\n        try:\n            print(f\"Loading model: {model_path}\")\n            checkpoint = torch.load(model_path, map_location=torch.device(cfg.device))\n            \n            model = BirdCLEFModel(cfg, num_classes)\n            model.load_state_dict(checkpoint['model_state_dict'])\n            model = model.to(cfg.device)\n            model.eval()\n            \n            models.append(model)\n        except Exception as e:\n            print(f\"Error loading model {model_path}: {e}\")\n    \n    return models\n\ndef predict_on_spectrogram(audio_path, models, cfg, species_ids):\n    \"\"\"Process a single audio file and predict species presence for each 5-second segment\"\"\"\n    predictions = []\n    row_ids = []\n    soundscape_id = Path(audio_path).stem\n    \n    try:\n        # print(f\"Processing {soundscape_id}\")\n        audio_data, _ = librosa.load(audio_path, sr=cfg.FS)\n        \n        total_segments = int(len(audio_data) / (cfg.FS * cfg.WINDOW_SIZE))\n        \n        for segment_idx in range(total_segments):\n            start_sample = segment_idx * cfg.FS * cfg.WINDOW_SIZE\n            end_sample = start_sample + cfg.FS * cfg.WINDOW_SIZE\n            segment_audio = audio_data[start_sample:end_sample]\n            \n            end_time_sec = (segment_idx + 1) * cfg.WINDOW_SIZE\n            row_id = f\"{soundscape_id}_{end_time_sec}\"\n            row_ids.append(row_id)\n\n            if cfg.use_tta:\n                all_preds = []\n                \n                for tta_idx in range(cfg.tta_count):\n                    mel_spec = process_audio_segment(segment_audio, cfg)\n                    mel_spec = apply_tta(mel_spec, tta_idx)\n\n                    mel_spec = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                    mel_spec = mel_spec.to(cfg.device)\n\n                    if len(models) == 1:\n                        with torch.no_grad():\n                            outputs = models[0](mel_spec)\n                            probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                            all_preds.append(probs)\n                    else:\n                        segment_preds = []\n                        for model in models:\n                            with torch.no_grad():\n                                outputs = model(mel_spec)\n                                probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                                segment_preds.append(probs)\n                        \n                        avg_preds = np.mean(segment_preds, axis=0)\n                        all_preds.append(avg_preds)\n\n                final_preds = np.mean(all_preds, axis=0)\n            else:\n                mel_spec = process_audio_segment(segment_audio, cfg)\n                \n                mel_spec = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                mel_spec = mel_spec.to(cfg.device)\n                \n                if len(models) == 1:\n                    with torch.no_grad():\n                        outputs = models[0](mel_spec)\n                        final_preds = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                else:\n                    segment_preds = []\n                    for model in models:\n                        with torch.no_grad():\n                            outputs = model(mel_spec)\n                            probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                            segment_preds.append(probs)\n\n                    final_preds = np.mean(segment_preds, axis=0)\n                    \n            predictions.append(final_preds)\n            \n    except Exception as e:\n        print(f\"Error processing {audio_path}: {e}\")\n    \n    return row_ids, predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T13:56:25.934115Z","iopub.execute_input":"2025-05-17T13:56:25.934425Z","iopub.status.idle":"2025-05-17T13:56:25.947525Z","shell.execute_reply.started":"2025-05-17T13:56:25.934396Z","shell.execute_reply":"2025-05-17T13:56:25.946794Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_tta(spec, tta_idx):\n    \"\"\"Apply test-time augmentation\"\"\"\n    if tta_idx == 0:\n        # Original spectrogram\n        return spec\n    elif tta_idx == 1:\n        # Time shift (horizontal flip)\n        return np.flip(spec, axis=1)\n    elif tta_idx == 2:\n        # Frequency shift (vertical flip)\n        return np.flip(spec, axis=0)\n    else:\n        return spec\n\ndef get_target(row):\n    target = encode_label(row['primary_label'])\n    if 'secondary_labels' in row and row['secondary_labels'] not in [[''], None, np.nan]:\n        if isinstance(row['secondary_labels'], str):\n            secondary_labels = eval(row['secondary_labels'])\n        else:\n            secondary_labels = row['secondary_labels']\n        \n        for label in secondary_labels:\n            if label in label_to_idx:\n                target[label_to_idx[label]] = 1.0\n    return target\n\ndef encode_label(label):\n    \"\"\"Encode label to one-hot vector\"\"\"\n    target = np.zeros(num_classes)\n    if label in label_to_idx:\n        target[label_to_idx[label]] = 1.0\n    return target\n    \ndef run_inference(cfg, models, species_ids):\n    \"\"\"Run inference on all test soundscapes\"\"\"\n    # test_files = list(Path(cfg.test_soundscapes).glob('*.ogg'))\n    df = pd.read_csv(cfg.train_csv)\n    \n    skf = StratifiedKFold(n_splits=cfg.n_fold, shuffle=True, random_state=cfg.seed)\n    _, val_idx = next(skf.split(df, df['primary_label']))\n    test_files = df.iloc[val_idx][m_empty]\n    if cfg.debug:\n        print(f\"Debug mode enabled, using only {cfg.debug_count} files\")\n        test_files = test_files.iloc[:cfg.debug_count]\n    \n    # print(f\"Found {len(test_files)} test soundscapes\")\n\n    all_row_ids = []\n    all_predictions = []\n    all_targets = []\n\n    for i, row in tqdm(test_files.iterrows(), total=len(test_files)):\n        audio_path = f\"{cfg.train_path}/{Path(row['filename'])}\"\n        row_ids, predictions = predict_on_spectrogram(str(audio_path), models, cfg, species_ids)\n        targets = np.repeat(row['primary_label'], len(row_ids), axis=0)\n        # targets = np.repeat(get_target(row), len(row_ids), axis=0)\n        all_row_ids.extend(row_ids)\n        all_targets.extend(targets)\n        \n        all_predictions\n        all_predictions.extend(predictions)\n    \n    return all_row_ids, all_predictions, all_targets\n\ndef create_submission(row_ids, predictions, species_ids, cfg):\n    \"\"\"Create submission dataframe\"\"\"\n    print(\"Creating submission dataframe...\")\n\n    submission_dict = {'row_id': row_ids}\n    \n    for i, species in enumerate(species_ids):\n        submission_dict[species] = [pred[i] for pred in predictions]\n\n    submission_df = pd.DataFrame(submission_dict)\n\n    submission_df.set_index('row_id', inplace=True)\n\n    sample_sub = pd.read_csv(cfg.submission_csv, index_col='row_id')\n\n    missing_cols = set(sample_sub.columns) - set(submission_df.columns)\n    if missing_cols:\n        print(f\"Warning: Missing {len(missing_cols)} species columns in submission\")\n        for col in missing_cols:\n            submission_df[col] = 0.0\n\n    submission_df = submission_df[sample_sub.columns]\n\n    submission_df = submission_df.reset_index()\n    \n    return submission_df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T13:56:25.948393Z","iopub.execute_input":"2025-05-17T13:56:25.948749Z","iopub.status.idle":"2025-05-17T13:56:25.969930Z","shell.execute_reply.started":"2025-05-17T13:56:25.948728Z","shell.execute_reply":"2025-05-17T13:56:25.969181Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# def main():\nstart_time = time.time()\nprint(\"Starting BirdCLEF-2025 inference...\")\nprint(f\"TTA enabled: {cfg.use_tta} (variations: {cfg.tta_count if cfg.use_tta else 0})\")\n\nmodels = load_models(cfg, num_classes)\n\nassert models, \"No models found! Please check model paths.\"\n\nprint(f\"Model usage: {'Single model' if len(models) == 1 else f'Ensemble of {len(models)} models'}\")\n\nrow_ids, predictions, targets = run_inference(cfg, models, species_ids)\n\nprint(row_ids)\n\nsubmission_df = create_submission(row_ids, predictions, species_ids, cfg)\n\nsubmission_path = 'submission.csv'\nsubmission_df.to_csv(submission_path, index=False)\nprint(f\"Submission saved to {submission_path}\")\n\nend_time = time.time()\nprint(f\"Inference completed in {(end_time - start_time)/60:.2f} minutes\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T13:56:25.971271Z","iopub.execute_input":"2025-05-17T13:56:25.971491Z","iopub.status.idle":"2025-05-17T14:08:17.644960Z","shell.execute_reply.started":"2025-05-17T13:56:25.971472Z","shell.execute_reply":"2025-05-17T14:08:17.644149Z"},"_kg_hide-output":true,"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# targ = np.reshape(targets, (-1, num_classes))\ntarg = np.array(targets)\npred = np.array(predictions)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T14:08:45.711520Z","iopub.execute_input":"2025-05-17T14:08:45.711825Z","iopub.status.idle":"2025-05-17T14:08:45.743962Z","shell.execute_reply.started":"2025-05-17T14:08:45.711803Z","shell.execute_reply":"2025-05-17T14:08:45.743096Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# sns.histplot(y_true)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T14:08:17.678299Z","iopub.execute_input":"2025-05-17T14:08:17.678517Z","iopub.status.idle":"2025-05-17T14:08:17.681547Z","shell.execute_reply.started":"2025-05-17T14:08:17.678497Z","shell.execute_reply":"2025-05-17T14:08:17.680930Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_true = targ\ny_pred = [species_ids[i] for i in np.argmax(pred, axis=1)]\ncm = confusion_matrix(y_true, y_pred, labels=np.unique(y_true))\n\n# normalize\nfreq = cm.sum(axis=1, keepdims=True)\nfreq[freq == 0] = 1\ncm_norm = cm / freq\n\nm_class = taxonomy_df['class_name'] == 'Insecta'\nlabels = taxonomy_df.apply(lambda x: f\"\"\"{x[\"common_name\"]} ({x[\"primary_label\"]})\"\"\", axis=1)\n\n# sns.heatmap(cm, annot=False)\nfig = go.Figure(data=go.Heatmap(\n    z=cm,\n    x=labels,\n    y=labels,\n    colorscale='Viridis',\n    hovertemplate='True: %{y}<br>Pred: %{x}<br>Count: %{z}<extra></extra>'\n))\n\nfig.update_layout(\n    title='Confusion Matrix Errors (Off-diagonal)',\n    xaxis_title='Predicted',\n    yaxis_title='True',\n    xaxis_tickangle=45,\n    width=700,\n    height=700\n)\n\nfig.show()\n\nnp.fill_diagonal(cm, 0)\nfig = go.Figure(data=go.Heatmap(\n    z=cm,\n    x=labels,\n    y=labels,\n    colorscale='Viridis',\n    hovertemplate='True: %{y}<br>Pred: %{x}<br>Count: %{z}<extra></extra>'\n))\n\nfig.update_layout(\n    title='Confusion Matrix Errors (Off-diagonal)',\n    xaxis_title='Predicted',\n    yaxis_title='True',\n    xaxis_tickangle=45,\n    width=700,\n    height=700\n)\n\nfig.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T15:41:54.462982Z","iopub.execute_input":"2025-05-17T15:41:54.463283Z","iopub.status.idle":"2025-05-17T15:41:54.551191Z","shell.execute_reply.started":"2025-05-17T15:41:54.463258Z","shell.execute_reply":"2025-05-17T15:41:54.550469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.read_csv(cfg.train_csv)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T18:04:44.246502Z","iopub.execute_input":"2025-05-17T18:04:44.246832Z","iopub.status.idle":"2025-05-17T18:04:44.418229Z","shell.execute_reply.started":"2025-05-17T18:04:44.246803Z","shell.execute_reply":"2025-05-17T18:04:44.417318Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## np.random.seed(43)\ndef sample_species(label):\n    # mask = df['collection'] == 'XC'\n    mask = df['primary_label'] == label\n    df_species = df[mask]\n    print(len(df_species))\n    sample = df_species.sample()\n    file_name = f\"/kaggle/input/birdclef-2025/train_audio/{sample.filename.item()}\"\n\n    # file_name = np.random.choice(list(Path(cfg.test_soundscapes).glob('*.ogg')))\n    y, sr = librosa.load(file_name, sr=32e3)\n    \n    display(sample)\n    \n    cutoff = 200\n    b, a = butter(4, cutoff/(sr/2), btype='highpass')\n    y = filtfilt(b, a, y)\n\n    display(ipd.Audio(y, rate=sr))\n    S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)\n    S_dB = librosa.power_to_db(S, ref=np.max)\n    hop_length = 512\n    frames = np.arange(S_dB.shape[1])\n    times = librosa.frames_to_time(frames, sr=sr, hop_length=hop_length)\n    fig = px.imshow(S_dB, x=times, aspect='auto', origin='lower')\n    fig.show()\n\n# sample_species('crebob1')\n# sample_species('grysee1')\n# sample_species('ywcpar')\nsample_species('piwtyr1')\n# sample_species('cattyr')\n# sample_species('spepar1')","metadata":{"execution":{"iopub.status.busy":"2025-05-17T19:24:07.283547Z","iopub.execute_input":"2025-05-17T19:24:07.283836Z","iopub.status.idle":"2025-05-17T19:24:07.588741Z","shell.execute_reply.started":"2025-05-17T19:24:07.283814Z","shell.execute_reply":"2025-05-17T19:24:07.587602Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"m_empty = df[\"secondary_labels\"] == \"['']\"\ndf['filename'].map(lambda x: x.split('/')[1]).duplicated()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T19:17:29.889634Z","iopub.execute_input":"2025-05-17T19:17:29.889924Z","iopub.status.idle":"2025-05-17T19:17:29.909951Z","shell.execute_reply.started":"2025-05-17T19:17:29.889900Z","shell.execute_reply":"2025-05-17T19:17:29.908925Z"}},"outputs":[],"execution_count":null}]}