{"record":{"id":"e71dff4725e9d291","repo":"pandas-dev/pandas","slug":"groupby-first-last-only-supports-1d-extensionarray","errorCode":null,"errorMessage":"groupby first/last only supports 1D ExtensionArrays","messagePattern":"groupby first/last only supports 1D ExtensionArrays","errorType":"exception","errorClass":"NotImplementedError","httpStatus":null,"severity":"error","filePath":"pandas/core/arrays/base.py","lineNumber":3208,"sourceCode":"\n    def _groupby_first_last(\n        self,\n        *,\n        how: str,\n        min_count: int,\n        ngroups: int,\n        ids: npt.NDArray[np.intp],\n        skipna: bool = True,\n    ) -> Self:\n        \"\"\"\n        Optimized implementation of groupby first/last for ExtensionArrays.\n\n        Uses an index-based approach: computes the index of the first/last\n        non-NA element per group, then gathers results via take(). This avoids\n        any dtype conversion and works for all EA types.\n        \"\"\"\n        if self.ndim != 1:\n            raise NotImplementedError(\n                \"groupby first/last only supports 1D ExtensionArrays\"\n            )\n        isna_mask = np.asarray(self.isna(), dtype=np.uint8)\n\n        result_indices, result_mask = libgroupby.group_first_last_indexer(\n            labels=ids,\n            mask=isna_mask,\n            ngroups=ngroups,\n            skipna=skipna,\n            is_last=(how == \"last\"),\n        )\n\n        # Apply min_count: require at least min_count non-NA observations.\n        # For first/last, the natural minimum is 1 (need at least one value).\n        if min_count > 1:\n            nobs = np.zeros(ngroups, dtype=np.int64)\n            non_na_indices = np.where((~isna_mask.view(bool)) & (ids >= 0))[0]\n            np.add.at(nobs, ids[non_na_indices], 1)","sourceCodeStart":3190,"sourceCodeEnd":3226,"githubUrl":"https://github.com/pandas-dev/pandas/blob/71959b8cb9b2459c16e14b34f28b178ccfe14735/pandas/core/arrays/base.py#L3190-L3226","documentation":"ExtensionArray._groupby_first_last (base.py:3208) only handles 1-D arrays; if self.ndim != 1 it raises NotImplementedError. The optimized first/last algorithm relies on per-group index lookup that is inherently 1-D.","triggerScenarios":"Triggered internally when pandas dispatches groupby first/last to an ExtensionArray whose ndim is not 1 (e.g. a multi-dimensional EA). Mostly an implementer/internal path; ordinary 1-D Series/columns never hit this.","commonSituations":"A custom ExtensionArray that is genuinely multi-dimensional (ndim>1) being used in a groupby first/last context; experimental stacked-array extensions.","solutions":["Ensure the ExtensionArray is 1-D (ndim == 1) before groupby first/last.","Flatten or split a multi-dimensional EA into 1-D arrays/Series.","Override _groupby_first_last in your subclass if you genuinely need N-D handling.","Fall back to a non-optimized path via apply() if N-D semantics are required."],"exampleFix":"# before: custom EA with ndim == 2 hits first/last dispatch\n# raises 'groupby first/last only supports 1D ExtensionArrays'\n\n# after\nfor col in range(ea.shape[1]):\n    ea_1d = ea[:, col]  # split into 1-D\n    ...  # then groupby first/last","handlingStrategy":"validation","validationCode":"def ensure_1d_ea(arr):\n    import numpy as np\n    if arr.ndim != 1:\n        raise ValueError(f\"expected 1D EA, got ndim={arr.ndim}\")\n    return arr","typeGuard":"def is_1d(arr) -> bool:\n    return getattr(arr, \"ndim\", 1) == 1","tryCatchPattern":"try:\n    grouped = grouped_obj.first()\nexcept NotImplementedError as e:\n    if \"only supports 1D\" in str(e):\n        # flatten or split N-D EA first\n        grouped = [arr[:, i] for i in range(arr.shape[1])]\n    else:\n        raise","preventionTips":["Keep ExtensionArrays 1-D for groupby first/last","Flatten N-D EAs before groupby","Override _groupby_first_last for custom N-D EAs"],"tags":["groupby","extension-array","first-last","ndim","internal"],"analyzedSha":"71959b8cb9b2459c16e14b34f28b178ccfe14735","analyzedAt":"2026-08-07T01:30:20.476Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}