{"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/3b7651241d4da534b3559b60ef128e1c34f54116/pandas/core/arrays/base.py#L3190-L3226","documentation":"Raised by ExtensionArray._groupby_first_last when self.ndim != 1. The optimized index-based first/last implementation uses libgroupby.group_first_last_indexer which only handles 1D arrays, so 2D (or higher) ExtensionArrays are rejected rather than silently producing wrong-shaped output.","triggerScenarios":"Calling groupby(...).first() or groupby(...).last() on a 2D ExtensionArray subclass (e.g. a custom EA with ndarray-shaped elements where ndim > 1). Reached when an ExtensionArray overrides __new__ to store 2D data, or when a third-party EA reports ndim=2.","commonSituations":"Custom third-party ExtensionArray types that wrap 2D data; experimental EAs for image/tensor data; subclassing ExtensionArray without keeping data 1D; version upgrades where this guard was added to fix silent shape bugs.","solutions":["Flatten the ExtensionArray to 1D before groupby first/last, or operate per-column.","If you control the EA subclass, ensure ndim always returns 1 for the array passed to groupby.","Fall back to a non-EA representation (e.g. numpy object array) for the groupby operation.","Override _groupby_first_last in your concrete EA subclass to handle the 2D case yourself."],"exampleFix":"// before\ngrouped = df.groupby('key')[tensor_ea_2d_column].first()\n// after\nfor col in tensor_ea_2d_column.flat_columns:\n    df.groupby('key')[col].first()","handlingStrategy":"validation","validationCode":"assert arr.ndim == 1, f'expected 1D EA, got ndim={arr.ndim}'","typeGuard":"def is_1d_ea(arr) -> bool:\n    return isinstance(arr, ExtensionArray) and arr.ndim == 1","tryCatchPattern":"try:\n    grouped.first()\nexcept NotImplementedError as e:\n    if '1D' in str(e):\n        ...","preventionTips":["Keep EA data 1D for groupby first/last.","Override _groupby_first_last if your EA is genuinely multi-dimensional."],"tags":["groupby","first","last","extension-array","ndim","notimplemented"],"backgroundTag":null,"analyzedSha":"3b7651241d4da534b3559b60ef128e1c34f54116","analyzedAt":"2026-08-11T22:10:44.015Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}