{"nbformat":4,"nbformat_minor":4,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyPek0zym4UYgblo7o0imocg"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":127282,"databundleVersionId":15520105,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"cells":[{"cell_type":"markdown","source":"Study Dataset : AnimalCLEF2026\n=====\n\nThe purpose of this notebook is to study the implicit data pattern in the given dataset.","metadata":{"id":"N6b2Ds4ucDOp"}},{"cell_type":"markdown","source":"# Setup","metadata":{"id":"83Lxv_l2XIeC"}},{"cell_type":"markdown","source":"## Dependent Libraries","metadata":{"id":"Ty3I4ps1bjTi"}},{"cell_type":"code","execution_count":null,"metadata":{"id":"XnGiGTB2Wcja","executionInfo":{"status":"ok","timestamp":1771154352875,"user_tz":-540,"elapsed":41075,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"48110eab-e365-4e5a-eb4a-aaa0d68d4844"},"outputs":[],"source":"!pip install wildlife-datasets git+https://github.com/WildlifeDatasets/wildlife-tools --quiet --upgrade-strategy only-if-needed"},{"cell_type":"code","source":"# Shared in Starter Notebook\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport torchvision.transforms as T\nimport timm\nfrom transformers import AutoModel\nfrom sklearn.cluster import DBSCAN\nfrom wildlife_datasets.datasets import AnimalCLEF2026\nfrom wildlife_tools.features import DeepFeatures\nfrom wildlife_tools.similarity import CosineSimilarity","metadata":{"id":"OKewEphvXRiK","executionInfo":{"status":"ok","timestamp":1771163718112,"user_tz":-540,"elapsed":35988,"user":{"displayName":"内田均","userId":"14810896148061948622"}}},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import os\nimport sys\nfrom pathlib import Path","metadata":{"id":"OsgPRYIiZRPw","executionInfo":{"status":"ok","timestamp":1771163718156,"user_tz":-540,"elapsed":5,"user":{"displayName":"内田均","userId":"14810896148061948622"}}},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Download Dataset 1.94GB","metadata":{"id":"zgLrREVAbfRe"}},{"cell_type":"code","source":"from google.colab import userdata\n\n# Replace with your Kaggle username and API key\nos.environ['KAGGLE_USERNAME'] = userdata.get('KAGGLE_USERNAME')\nos.environ['KAGGLE_KEY'] = userdata.get('KAGGLE_KEY')\n\n!kaggle competitions download -c animal-clef-2026\n!unzip -q animal-clef-2026.zip","metadata":{"id":"SMNiMwRsao_M","executionInfo":{"status":"ok","timestamp":1771154540424,"user_tz":-540,"elapsed":163405,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"c960840b-b4f2-4f59-8981-671b5915bd8d"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Visualizing Data\n\nCodes of this section was provided by the host of this project [@picekl](https://www.kaggle.com/picekl) at [starter notebook](https://www.kaggle.com/code/picekl/animalclef2026-starter-notebook?scriptVersionId=295889821&cellId=6).","metadata":{"id":"256F6rveYJcx"}},{"cell_type":"code","source":"import os\nimport zipfile\nfrom pathlib import Path\n\nroot = '/content/' # Changed path to a writable location\n\ndataset_full = AnimalCLEF2026(\n    root,\n    transform=None,\n    load_label=True,\n    factorize_label=True,\n    check_files=True # Keeping this as False, assuming files are now present\n)\nprint(\"Dataset loaded successfully.\")","metadata":{"id":"-7Nrrg39YMsA","executionInfo":{"status":"ok","timestamp":1771163719209,"user_tz":-540,"elapsed":999,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"a82c9e4a-1639-404c-bbdb-5f0802754d68"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"dataset_full.metadata.head()","metadata":{"id":"JCnwi0f_YaW-","executionInfo":{"status":"ok","timestamp":1771163719418,"user_tz":-540,"elapsed":173,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"c529c410-ace2-4ee8-dbee-24a7aecf02bd"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"dataset_full.metadata[['dataset', 'split']].value_counts(sort=False)","metadata":{"id":"hEtxecaPdhTb","executionInfo":{"status":"ok","timestamp":1771163719463,"user_tz":-540,"elapsed":42,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"b0408940-1df0-40f3-d38b-3b553d035fef"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"datasets = {}\nfor name in dataset_full.metadata['dataset'].unique():\n    datasets[name] = dataset_full.get_subset(dataset_full.df['dataset'] == name)\ndatasets","metadata":{"id":"gTxyIrxIigc-","executionInfo":{"status":"ok","timestamp":1771163719476,"user_tz":-540,"elapsed":17,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"ccc48ece-efcc-4f51-9a9d-0333bd36a3dc"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"for dataset in datasets.values():\n    dataset.plot_grid(n_rows=3, n_cols=4)","metadata":{"id":"PkjTylgIDMjc","executionInfo":{"status":"ok","timestamp":1771163726730,"user_tz":-540,"elapsed":7250,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"bd4df8b0-6eb1-4af9-8e0d-8f2215a81b43"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Statistics Features","metadata":{"id":"fVgurj_DVs_Y"}},{"cell_type":"markdown","source":"Let's seperate into train / test and see the data patterns","metadata":{"id":"90wNS9Lf4ImS"}},{"cell_type":"code","source":"dataset_train = dataset_full.get_subset(dataset_full.df['split'] == 'train')\ndataset_test = dataset_full.get_subset(dataset_full.df['split'] == 'test')\nlen(dataset_train), len(dataset_test)","metadata":{"id":"8fimHoADGfZr","executionInfo":{"status":"ok","timestamp":1771163726749,"user_tz":-540,"elapsed":10,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"c2e05507-a973-4f1d-9acb-00abd83c59a6"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"check the number of nan in each feature","metadata":{"id":"xLsLMYFL4Yne"}},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(8, 4))\nfig.suptitle('Number of NaN')\n\naxes[0].bar(dataset_train.metadata.keys(), dataset_train.metadata.isna().sum())\naxes[0].set_xticks(\n    dataset_train.metadata.keys(), labels=dataset_train.metadata.keys(), rotation=45)\naxes[0].set_title('Train')\naxes[0].set_ylabel('Number of NaN')\n\naxes[1].bar(dataset_test.metadata.keys(), dataset_test.metadata.isna().sum())\naxes[1].set_xticks(\n    dataset_test.metadata.keys(), labels=dataset_test.metadata.keys(), rotation=45)\naxes[1].set_title('Test')\n\nplt.tight_layout()\nplt.show()","metadata":{"id":"OaRCcPopE0k_","executionInfo":{"status":"ok","timestamp":1771168731023,"user_tz":-540,"elapsed":656,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"49f66a47-6f44-4446-ee6a-b46d844bb86b"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Individual Code 'Identity'","metadata":{"id":"RLAwE_t0KjF9"}},{"cell_type":"code","source":"def pie_topk(dataset, key, ax, topk=None, startangle=90):\n    vc = dataset.metadata[key].value_counts(dropna=False)\n    topk = topk if topk else len(vc)\n    labels = [''] * len(vc)\n    for i in range(min(len(vc), topk)):\n        ratio = vc.values[i] / vc.values.sum() * 100.0\n        labels[i] = f'{vc.index[i]}: {ratio:.1f}%'\n    ax.pie(vc.values, labels=labels, startangle=startangle)","metadata":{"id":"3csux1X0b5g-","executionInfo":{"status":"ok","timestamp":1771163727162,"user_tz":-540,"elapsed":14,"user":{"displayName":"内田均","userId":"14810896148061948622"}}},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(8, 4))\nfig.suptitle('Identity Distribution')\n\npie_topk(dataset_train, 'identity', axes[0], topk=5, startangle=0)\naxes[0].set_title(f'Train: {len(dataset_train)}')\n\npie_topk(dataset_test, 'identity', axes[1], topk=2)\naxes[1].set_title(f'Test: {len(dataset_test)}')\n\nplt.tight_layout()\nplt.show()","metadata":{"id":"DVFnappdVN4f","executionInfo":{"status":"ok","timestamp":1771168811939,"user_tz":-540,"elapsed":6036,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"a9de406e-3f9d-4d40-846e-3c05bc25ec22"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# prefix = part before first \"_\"\ndataset_train.metadata[\"prefix\"] = dataset_train.metadata[\"identity\"].astype(str).str.split(\"_\").str[0]\ndataset_test.metadata[\"prefix\"]  = dataset_test.metadata[\"identity\"].astype(str).str.split(\"_\").str[0]\n\nfig, axes = plt.subplots(1, 2, figsize=(8, 4))\nfig.suptitle('Species Distribution in a column \"identity\"')\n\n# if you want to use your helper:\npie_topk(dataset_train, 'prefix', axes[0], topk=10, startangle=0)\naxes[0].set_title(f'Train: {len(dataset_train)}')\n\npie_topk(dataset_test, 'prefix', axes[1], topk=10, startangle=0)\naxes[1].set_title(f'Test: {len(dataset_test)}')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"id":"LwYUFP27_H2f","executionInfo":{"status":"ok","timestamp":1771170968997,"user_tz":-540,"elapsed":291,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"d5248634-a99f-419a-e01e-82f516713f08"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Shooting Date","metadata":{"id":"wGuP7XZ6TX3G"}},{"cell_type":"markdown","source":"### Year Distirbution\n\nIt seems almost of the photos of training data were shooted between 2015 - 2022, not recently though the ones in test data were recently taken. We may be able to improve the peformance of the model by considering the spec of the cameras shooting them because they influence to the qualities of image contents as well.","metadata":{"id":"yPGVf66Gze3H"}},{"cell_type":"code","source":"def hist_year(dataset, ax):\n    dataset.metadata['date'] = pd.to_datetime(dataset.metadata['date'], errors='coerce')\n    vc = dataset.metadata['date'].dt.year.value_counts(dropna=False)\n    vc.sort_index(inplace=True)\n    vc.plot(kind='bar', ax=ax)\n    ax.set_xlabel('Year')\n    ax.set_xticklabels(\n        [int(x) if pd.notna(x) else str(x) for x in vc.index], rotation=90)\n    ax.set_ylabel('Count')\n\nfig, axes = plt.subplots(1, 2, figsize=(10, 4))\nfig.suptitle('Year Distribution')\n\nhist_year(dataset_train, axes[0])\naxes[0].set_title(f'Train: n={len(dataset_train)}')\nhist_year(dataset_test, axes[1])\naxes[1].set_title(f'Test: n={len(dataset_test)}')\n\nplt.tight_layout()\nplt.show() # Call plt.show() once at the end","metadata":{"id":"zYkUJcErunXR","executionInfo":{"status":"ok","timestamp":1771169837288,"user_tz":-540,"elapsed":1235,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"b2bbee6f-58b8-4043-9876-fbebd9370fdf"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Unique Date\n\nThe raios of NaN are large.","metadata":{"id":"_GJEzztQzSTr"}},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(12, 6))\nfig.suptitle('Date Distribution')\n\n# train\npie_topk(dataset_train, 'date', axes[0], topk=5)\naxes[0].set_title(f'Train: n={len(dataset_train)}')\n\n# test\npie_topk(dataset_test, 'date', axes[1], topk=2)\naxes[1].set_title(f'Test: n={len(dataset_test)}')\n\nplt.show()","metadata":{"id":"4dkeZKuHTavD","executionInfo":{"status":"ok","timestamp":1771169948337,"user_tz":-540,"elapsed":2857,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"0a85c2ce-97da-4f2b-c83f-e353c98eedcc"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"##","metadata":{"id":"ePr7b9Fry65X"}},{"cell_type":"markdown","source":"## Shooting Orientation of the animal body\n\nThe primary orientations are 'left', 'right' and 'top'.\n\nIn test data, the ratio of NaN is 32% and it should be considred in predicting, for examples, fixing with the ratios of orientations of training data.","metadata":{"id":"QocjkdItV3vL"}},{"cell_type":"code","source":"def pie_color(dataset_train, dataset_test, colname, title):\n    # counts including nan -------------------------------------------------------------------\n    vc_train = dataset_train.metadata[colname].value_counts(dropna=False)\n    vc_test  = dataset_test.metadata[colname].value_counts(dropna=False)\n\n    # Setup colors per label -----------------------------------------------------------------\n    # consistent label universe (sorted for determinism)\n    # Convert index to string to handle NaN values for sorting\n    all_labels = sorted(set(vc_train.index.astype(str)).union(set(vc_test.index.astype(str))))\n\n    # assign colors once (same species -> same color)\n    cmap = plt.get_cmap('tab20')  # palette\n    color_map = {lab: cmap(i % cmap.N) for i, lab in enumerate(all_labels)}\n\n    def colors_for(vc):\n        return [color_map[str(lab)] for lab in vc.index]\n\n    # Show -----------------------------------------------------------------------------------\n    fig, axes = plt.subplots(1, 2, figsize=(12, 6))\n    fig.suptitle(title)\n\n    # train\n    axes[0].pie(\n        vc_train.values,\n        labels=vc_train.index.astype(str), # Convert labels to string for display\n        autopct='%1.1f%%',\n        colors=colors_for(vc_train),\n        # startangle=90\n    )\n    axes[0].set_title(f'Train: n={len(dataset_train)}')\n\n    # test\n    axes[1].pie(\n        vc_test.values,\n        labels=vc_test.index.astype(str), # Convert labels to string for display\n        autopct='%1.1f%%',\n        colors=colors_for(vc_test),\n        # startangle=90\n    )\n    axes[1].set_title(f'Test: n={len(dataset_test)}')\n\n    plt.show()\n\npie_color(dataset_train, dataset_test, 'orientation', 'Orientation Distribution')","metadata":{"id":"4cQCsuSNlwEW","executionInfo":{"status":"ok","timestamp":1771169983998,"user_tz":-540,"elapsed":772,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"c17b2e13-bd85-4846-924e-fbcc97d16f7f"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Statistics of Species\n\nThe ratio of species is very different between train and test.\n\nIn training data, 🦎 Salamandra salamandra is not included in a column 'species' though we saw in another column 'identity'.","metadata":{"id":"R_by512LRsJp"}},{"cell_type":"code","source":"pie_color(dataset_train, dataset_test, 'species', 'Species Distribution')","metadata":{"id":"DTA1BY7HneDE","executionInfo":{"status":"ok","timestamp":1771164275387,"user_tz":-540,"elapsed":524,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"68e102db-0a82-4d14-9b0b-66d26f3233b8"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Regarding instances whose species is NaN in train data, they are *🦎 Salamandra salamandra*.\n","metadata":{"id":"ekYbOxN8EJAP"}},{"cell_type":"code","source":"dataset_train.metadata[dataset_train.metadata['species'].isna()]['prefix'].value_counts()","metadata":{"id":"d2sPV1CvDmbC","executionInfo":{"status":"ok","timestamp":1771171699234,"user_tz":-540,"elapsed":45,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"693b3cfd-20d4-43c6-a694-5ca3e613f4e7"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Image Contents\n\nLet's investigate the resolution and brightness of images.","metadata":{"id":"PRfShQ7teyBE"}},{"cell_type":"code","source":"from pathlib import Path\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image, ImageOps\nfrom tqdm.auto import tqdm\n\ndef iter_image_paths(root: str | Path, exts=(\".jpg\", \".jpeg\", \".png\", \".bmp\", \".webp\")):\n    root = Path(root)\n    for p in root.rglob(\"*\"):\n        if p.is_file() and p.suffix.lower() in exts:\n            yield p\n\ndef analyze_image_pil(img: Image.Image, dark_thr=40, bright_thr=215):\n    w, h = img.size\n    g = ImageOps.grayscale(img)\n    arr = np.asarray(g, dtype=np.uint8)\n\n    mean_lum = float(arr.mean())\n    std_lum  = float(arr.std())\n    p_dark   = float((arr < dark_thr).mean())\n    p_bright = float((arr > bright_thr).mean())\n\n    is_dark = (mean_lum < 60) or (p_dark > 0.60)\n\n    return dict(\n        width=w, height=h, aspect=w / h if h else np.nan,\n        mean_lum=mean_lum, std_lum=std_lum,\n        p_dark=p_dark, p_bright=p_bright,\n        is_dark=bool(is_dark),\n        mode=img.mode,\n    )\n\ndef parse_dataset_split(path: Path, root: Path):\n    \"\"\"\n    For: images/<DatasetName>/(train|test)/...\n    returns (dataset, split)\n    \"\"\"\n    rel = path.relative_to(root)\n    parts = rel.parts\n    dataset = parts[0] if len(parts) >= 1 else \"UNKNOWN\"\n    split = parts[1] if len(parts) >= 2 else \"UNKNOWN\"   # train/test\n    return dataset, split\n\ndef scan_images_with_split(root=\"images\", max_images=None):\n    root = Path(root)\n    paths = list(iter_image_paths(root))\n    if not paths:\n        raise ValueError(f\"No images found under {root}\")\n\n    if max_images is not None:\n        paths = paths[: int(max_images)]\n\n    rows = []\n    for p in tqdm(paths, desc=\"Scanning\"):\n        dataset, split = parse_dataset_split(p, root)\n        try:\n            with Image.open(p) as img:\n                img.load()\n                stats = analyze_image_pil(img)\n            rows.append({\n                \"path\": str(p),\n                \"dataset\": dataset,\n                \"split\": split,\n                **stats\n            })\n        except Exception as e:\n            rows.append({\n                \"path\": str(p),\n                \"dataset\": dataset,\n                \"split\": split,\n                \"error\": repr(e)\n            })\n\n    df = pd.DataFrame(rows)\n    if \"error\" not in df.columns:\n        df[\"error\"] = np.nan\n    return df\n\ndf = scan_images_with_split(root=\"images\")\ngood = df[df[\"error\"].isna()].copy()\n","metadata":{"id":"f24moRpYgrjf"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Regarding the resolution, test data is larger than training data because we found the shooting date was newer and higher spec of cameras would be used in test data.\n\nRegarding photos of sea turtle, the resolution is lower than others, especially in training data.\n\nRegarding the darkness, it is very high in lynx, but it is drived from the background color of the masking.","metadata":{"id":"KuXMJAECF-2o"}},{"cell_type":"code","source":"# Overall by train/test\nsummary_split = (good.groupby(\"split\")\n                 .agg(n=(\"path\",\"count\"),\n                      mean_w=(\"width\",\"mean\"),\n                      mean_h=(\"height\",\"mean\"),\n                      mean_aspect=(\"aspect\",\"mean\"),\n                      dark_rate=(\"is_dark\",\"mean\"),\n                      mean_lum=(\"mean_lum\",\"mean\"))\n                 .sort_index())\ndisplay(summary_split)\n\n# By dataset\nsummary_dataset_split = (good.groupby([\"dataset\",\"split\"])\n                         .agg(n=(\"path\",\"count\"),\n                              mean_w=(\"width\",\"mean\"),\n                              mean_h=(\"height\",\"mean\"),\n                              mean_aspect=(\"aspect\",\"mean\"),\n                              dark_rate=(\"is_dark\",\"mean\"),\n                              mean_lum=(\"mean_lum\",\"mean\"))\n                         .sort_values([\"dataset\",\"split\"]))\ndisplay(summary_dataset_split)","metadata":{"id":"_BuaYAYKhA57","executionInfo":{"status":"ok","timestamp":1771167921185,"user_tz":-540,"elapsed":116,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"42a294db-acba-4a4d-e280-503c0c6035ce"},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\n\nfor split in [\"train\", \"test\"]:\n    d = good[good[\"split\"] == split]\n    if len(d) == 0:\n        continue\n\n    fig, axes = plt.subplots(2, 3, figsize=(16, 9))\n    fig.suptitle(f\"Image statistics: {split} (n={len(d)})\", fontsize=16)\n\n    # 1) Width\n    ax = axes[0, 0]\n    ax.hist(d[\"width\"], bins=50)\n    ax.set_title(\"Width distribution\")\n    ax.set_xlabel(\"width (px)\")\n    ax.set_ylabel(\"count\")\n\n    # 2) Height\n    ax = axes[0, 1]\n    ax.hist(d[\"height\"], bins=50)\n    ax.set_title(\"Height distribution\")\n    ax.set_xlabel(\"height (px)\")\n    ax.set_ylabel(\"count\")\n\n    # 3) Width vs Height\n    ax = axes[0, 2]\n    ax.scatter(d[\"width\"], d[\"height\"], s=5)\n    ax.set_title(\"Width vs Height\")\n    ax.set_xlabel(\"width (px)\")\n    ax.set_ylabel(\"height (px)\")\n\n    # 4) Mean luminance\n    ax = axes[1, 0]\n    ax.hist(d[\"mean_lum\"], bins=50)\n    ax.set_title(\"Mean luminance distribution\")\n    ax.set_xlabel(\"mean luminance (0..255)\")\n    ax.set_ylabel(\"count\")\n\n    # 5) Dark rate (bar)\n    ax = axes[1, 1]\n    counts = d[\"is_dark\"].value_counts().reindex([False, True], fill_value=0)\n    ax.bar([\"False\", \"True\"], counts.values)\n    ax.set_title(\"Is dark (count)\")\n    ax.set_xlabel(\"is_dark\")\n    ax.set_ylabel(\"count\")\n\n    # 6) text summary\n    ax = axes[1, 2]\n    ax.axis(\"off\")\n    dark_rate = d[\"is_dark\"].mean()\n    ax.text(\n        0.0, 1.0,\n        \"\\n\".join([\n            f\"n = {len(d)}\",\n            f\"unique sizes = {d[['width','height']].drop_duplicates().shape[0]}\",\n            f\"mean size = {d['width'].mean():.1f} × {d['height'].mean():.1f}\",\n            f\"dark rate = {dark_rate*100:.2f}%\",\n            f\"mean lum = {d['mean_lum'].mean():.2f} ± {d['mean_lum'].std():.2f}\",\n        ]),\n        va=\"top\", ha=\"left\", fontsize=12\n    )\n\n    plt.tight_layout(rect=[0, 0, 1, 0.95])\n    plt.show()\n","metadata":{"id":"aulF7i1food2","executionInfo":{"status":"ok","timestamp":1771164588241,"user_tz":-540,"elapsed":2879,"user":{"displayName":"内田均","userId":"14810896148061948622"}},"outputId":"c36c942f-750f-44e6-c4d8-cd65bedb9ed8"},"execution_count":null,"outputs":[]}]}