1from __future__ import annotations
2
3import itertools
4from typing import (
5 TYPE_CHECKING,
6 Literal,
7 cast,
8)
9
10import numpy as np
11
12from pandas._libs import lib
13from pandas.util._decorators import set_module
14
15from pandas.core.dtypes.cast import maybe_downcast_to_dtype
16from pandas.core.dtypes.common import (
17 is_list_like,
18 is_nested_list_like,
19 is_scalar,
20)
21from pandas.core.dtypes.dtypes import ExtensionDtype
22from pandas.core.dtypes.generic import (
23 ABCDataFrame,
24 ABCSeries,
25)
26
27import pandas.core.common as com
28from pandas.core.groupby import Grouper
29from pandas.core.indexes.api import (
30 Index,
31 MultiIndex,
32 get_objs_combined_axis,
33)
34from pandas.core.reshape.concat import concat
35from pandas.core.series import Series
36
37if TYPE_CHECKING:
38 from collections.abc import (
39 Callable,
40 Hashable,
41 )
42
43 from pandas._typing import (
44 AggFuncType,
45 AggFuncTypeBase,
46 AggFuncTypeDict,
47 IndexLabel,
48 SequenceNotStr,
49 )
50
51 from pandas import DataFrame
52 from pandas.api.typing import (
53 DataFrameGroupBy,
54 SeriesGroupBy,
55 )
56
57
58@set_module("pandas")
59def pivot_table(
60 data: DataFrame,
61 values=None,
62 index=None,
63 columns=None,
64 aggfunc: AggFuncType = "mean",
65 fill_value=None,
66 margins: bool = False,
67 dropna: bool = True,
68 margins_name: Hashable = "All",
69 observed: bool = True,
70 sort: bool = True,
71 **kwargs,
72) -> DataFrame:
73 """
74 Create a spreadsheet-style pivot table as a DataFrame.
75
76 The levels in the pivot table will be stored in MultiIndex objects
77 (hierarchical indexes) on the index and columns of the result DataFrame.
78
79 Parameters
80 ----------
81 data : DataFrame
82 Input pandas DataFrame object.
83 values : list-like or scalar, optional
84 Column or columns to aggregate.
85 index : column, Grouper, array, or sequence of the previous
86 Keys to group by on the pivot table index. If a list is passed,
87 it can contain any of the other types (except list). If an array is
88 passed, it must be the same length as the data and will be used in
89 the same manner as column values.
90 columns : column, Grouper, array, or sequence of the previous
91 Keys to group by on the pivot table column. If a list is passed,
92 it can contain any of the other types (except list). If an array is
93 passed, it must be the same length as the data and will be used in
94 the same manner as column values.
95 aggfunc : function, list of functions, dict, default "mean"
96 If a list of functions is passed, the resulting pivot table will have
97 hierarchical columns whose top level are the function names
98 (inferred from the function objects themselves).
99 If a dict is passed, the key is column to aggregate and the value is
100 function or list of functions. If ``margins=True``, aggfunc will be
101 used to calculate the partial aggregates.
102 fill_value : scalar, default None
103 Value to replace missing values with (in the resulting pivot table,
104 after aggregation).
105 margins : bool, default False
106 If ``margins=True``, special ``All`` columns and rows
107 will be added with partial group aggregates across the categories
108 on the rows and columns.
109 dropna : bool, default True
110 Do not include columns whose entries are all NaN. If True,
111
112 * rows with an NA value in any column will be omitted before computing margins,
113 * index/column keys containing NA values will be dropped (see ``dropna``
114 parameter in :meth:``DataFrame.groupby``).
115
116 margins_name : str, default 'All'
117 Name of the row / column that will contain the totals
118 when margins is True.
119 observed : bool, default True
120 This only applies if any of the groupers are Categoricals.
121 If True: only show observed values for categorical groupers.
122 If False: show all values for categorical groupers.
123
124 .. versionchanged:: 3.0.0
125
126 The default value is now ``True``.
127
128 sort : bool, default True
129 Specifies if the result should be sorted.
130
131 **kwargs : dict
132 Optional keyword arguments to pass to ``aggfunc``.
133
134 .. versionadded:: 3.0.0
135
136 Returns
137 -------
138 DataFrame
139 An Excel style pivot table.
140
141 See Also
142 --------
143 DataFrame.pivot : Pivot without aggregation that can handle
144 non-numeric data.
145 DataFrame.melt: Unpivot a DataFrame from wide to long format,
146 optionally leaving identifiers set.
147 wide_to_long : Wide panel to long format. Less flexible but more
148 user-friendly than melt.
149
150 Notes
151 -----
152 Reference :ref:`the user guide <reshaping.pivot>` for more examples.
153
154 .. versionchanged:: 3.1.0
155 When ``values`` is empty, which is the case when all columns of
156 ``data`` are either in ``index`` or ``columns`` arguments, aggregation
157 will act on a Series of all NA values.
158
159 Examples
160 --------
161 >>> df = pd.DataFrame(
162 ... {
163 ... "A": ["foo", "foo", "foo", "foo", "foo", "bar", "bar", "bar", "bar"],
164 ... "B": ["one", "one", "one", "two", "two", "one", "one", "two", "two"],
165 ... "C": [
166 ... "small",
167 ... "large",
168 ... "large",
169 ... "small",
170 ... "small",
171 ... "large",
172 ... "small",
173 ... "small",
174 ... "large",
175 ... ],
176 ... "D": [1, 2, 2, 3, 3, 4, 5, 6, 7],
177 ... "E": [2, 4, 5, 5, 6, 6, 8, 9, 9],
178 ... }
179 ... )
180 >>> df
181 A B C D E
182 0 foo one small 1 2
183 1 foo one large 2 4
184 2 foo one large 2 5
185 3 foo two small 3 5
186 4 foo two small 3 6
187 5 bar one large 4 6
188 6 bar one small 5 8
189 7 bar two small 6 9
190 8 bar two large 7 9
191
192 This first example aggregates values by taking the sum.
193
194 >>> table = pd.pivot_table(
195 ... df, values="D", index=["A", "B"], columns=["C"], aggfunc="sum"
196 ... )
197 >>> table
198 C large small
199 A B
200 bar one 4.0 5.0
201 two 7.0 6.0
202 foo one 4.0 1.0
203 two NaN 6.0
204
205 We can also fill missing values using the `fill_value` parameter.
206
207 >>> table = pd.pivot_table(
208 ... df, values="D", index=["A", "B"], columns=["C"], aggfunc="sum", fill_value=0
209 ... )
210 >>> table
211 C large small
212 A B
213 bar one 4 5
214 two 7 6
215 foo one 4 1
216 two 0 6
217
218 The next example aggregates by taking the mean across multiple columns.
219
220 >>> table = pd.pivot_table(
221 ... df, values=["D", "E"], index=["A", "C"], aggfunc={"D": "mean", "E": "mean"}
222 ... )
223 >>> table
224 D E
225 A C
226 bar large 5.500000 7.500000
227 small 5.500000 8.500000
228 foo large 2.000000 4.500000
229 small 2.333333 4.333333
230
231 We can also calculate multiple types of aggregations for any given
232 value column.
233
234 >>> table = pd.pivot_table(
235 ... df,
236 ... values=["D", "E"],
237 ... index=["A", "C"],
238 ... aggfunc={"D": "mean", "E": ["min", "max", "mean"]},
239 ... )
240 >>> table
241 D E
242 mean max mean min
243 A C
244 bar large 5.500000 9 7.500000 6
245 small 5.500000 9 8.500000 8
246 foo large 2.000000 5 4.500000 4
247 small 2.333333 6 4.333333 2
248 """
249 index = _convert_by(index)
250 columns = _convert_by(columns)
251
252 if isinstance(aggfunc, list):
253 pieces: list[DataFrame] = []
254 keys = []
255 for func in aggfunc:
256 _table = __internal_pivot_table(
257 data,
258 values=values,
259 index=index,
260 columns=columns,
261 fill_value=fill_value,
262 aggfunc=func,
263 margins=margins,
264 dropna=dropna,
265 margins_name=margins_name,
266 observed=observed,
267 sort=sort,
268 kwargs=kwargs,
269 )
270 pieces.append(_table)
271 keys.append(getattr(func, "__name__", func))
272
273 table = concat(pieces, keys=keys, axis=1)
274 return table.__finalize__(data, method="pivot_table")
275
276 table = __internal_pivot_table(
277 data,
278 values,
279 index,
280 columns,
281 aggfunc,
282 fill_value,
283 margins,
284 dropna,
285 margins_name,
286 observed,
287 sort,
288 kwargs,
289 )
290 return table.__finalize__(data, method="pivot_table")
291
292
293def __internal_pivot_table(
294 data: DataFrame,
295 values,
296 index,
297 columns,
298 aggfunc: AggFuncTypeBase | AggFuncTypeDict,
299 fill_value,
300 margins: bool,
301 dropna: bool,
302 margins_name: Hashable,
303 observed: bool,
304 sort: bool,
305 kwargs,
306) -> DataFrame:
307 """
308 Helper of :func:`pandas.pivot_table` for any non-list ``aggfunc``.
309 """
310 keys = index + columns
311
312 values_passed = values is not None
313 if values_passed:
314 if is_list_like(values):
315 values_multi = True
316 values = list(values)
317 else:
318 values_multi = False
319 values = [values]
320
321 # GH14938 Make sure value labels are in data
322 for i in values:
323 if i not in data:
324 raise KeyError(i)
325
326 to_filter = []
327 for x in keys + values:
328 if isinstance(x, Grouper):
329 x = x.key
330 try:
331 if x in data:
332 to_filter.append(x)
333 except TypeError:
334 pass
335 if len(to_filter) < len(data.columns):
336 data = data[to_filter]
337
338 else:
339 values = data.columns
340 for key in keys:
341 try:
342 values = values.drop(key)
343 except (TypeError, ValueError, KeyError):
344 pass
345 values = list(values)
346
347 grouped: SeriesGroupBy | DataFrameGroupBy = data.groupby(
348 keys, observed=observed, sort=sort, dropna=dropna
349 )
350 if values_passed:
351 # GH#57876 and GH#61292
352 grouped = grouped[values]
353 elif grouped._obj_with_exclusions.columns.empty:
354 grouped = Series(np.nan, index=data.index).groupby(
355 [data[key] for key in keys], observed=observed, sort=sort, dropna=dropna
356 )
357
358 agged = grouped.agg(aggfunc, **kwargs)
359
360 if dropna and isinstance(agged, ABCDataFrame) and len(agged.columns):
361 agged = agged.dropna(how="all")
362
363 table = agged
364
365 # GH17038, this check should only happen if index is defined (not None)
366 if table.index.nlevels > 1 and index:
367 # Related GH #17123
368 # If index_names are integers, determine whether the integers refer
369 # to the level position or name.
370 index_names = agged.index.names[: len(index)]
371 to_unstack = []
372 for i in range(len(index), len(keys)):
373 name = agged.index.names[i]
374 if name is None or name in index_names:
375 to_unstack.append(i)
376 else:
377 to_unstack.append(name)
378 table = agged.unstack(to_unstack, fill_value=fill_value)
379
380 if not dropna:
381 if isinstance(table.index, MultiIndex):
382 m = MultiIndex.from_product(table.index.levels, names=table.index.names)
383 table = table.reindex(m, axis=0, fill_value=fill_value)
384
385 if isinstance(table.columns, MultiIndex):
386 m = MultiIndex.from_product(table.columns.levels, names=table.columns.names)
387 table = table.reindex(m, axis=1, fill_value=fill_value)
388
389 if sort is True and isinstance(table, ABCDataFrame):
390 table = table.sort_index(axis=1)
391
392 if fill_value is not None:
393 table = table.fillna(fill_value)
394 if aggfunc is len and not observed and lib.is_integer(fill_value):
395 # TODO: can we avoid this? this used to be handled by
396 # downcast="infer" in fillna
397 table = table.astype(np.int64)
398
399 if margins:
400 if dropna:
401 data = data[data.notna().all(axis=1)]
402 table = _add_margins(
403 table,
404 data,
405 values,
406 rows=index,
407 cols=columns,
408 aggfunc=aggfunc,
409 kwargs=kwargs,
410 observed=dropna,
411 margins_name=margins_name,
412 fill_value=fill_value,
413 dropna=dropna,
414 )
415
416 # discard the top level
417 if values_passed and not values_multi and table.columns.nlevels > 1:
418 table.columns = table.columns.droplevel(0)
419 if len(index) == 0 and len(columns) > 0:
420 table = table.T
421
422 # GH 15193 Make sure empty columns are removed if dropna=True
423 if isinstance(table, ABCDataFrame) and dropna:
424 table = table.dropna(how="all", axis=1)
425
426 return cast("DataFrame", table)
427
428
429def _add_margins(
430 table: DataFrame | Series,
431 data: DataFrame,
432 values,
433 rows,
434 cols,
435 aggfunc,
436 kwargs,
437 observed: bool,
438 margins_name: Hashable = "All",
439 fill_value=None,
440 dropna: bool = True,
441):
442 if not isinstance(margins_name, str):
443 raise ValueError("margins_name argument must be a string")
444
445 msg = f'Conflicting name "{margins_name}" in margins'
446 for level in table.index.names:
447 if margins_name in table.index.get_level_values(level):
448 raise ValueError(msg)
449
450 grand_margin = _compute_grand_margin(data, values, aggfunc, kwargs, margins_name)
451
452 if table.ndim == 2:
453 # i.e. DataFrame
454 for level in table.columns.names[1:]:
455 if margins_name in table.columns.get_level_values(level):
456 raise ValueError(msg)
457
458 key: str | tuple[str, ...]
459 if len(rows) > 1:
460 key = (margins_name,) + ("",) * (len(rows) - 1)
461 else:
462 key = margins_name
463
464 if not values and isinstance(table, ABCSeries):
465 # If there are no values and the table is a series, then there is only
466 # one column in the data. Compute grand margin and return it.
467 row = table._constructor({key: grand_margin[margins_name]})
468 if not isinstance(table.index, MultiIndex):
469 row.index.name = table.index.name
470 return table._append_internal(row)
471
472 elif values:
473 marginal_result_set = _generate_marginal_results(
474 table,
475 data,
476 values,
477 rows,
478 cols,
479 aggfunc,
480 kwargs,
481 observed,
482 margins_name,
483 dropna,
484 )
485 if not isinstance(marginal_result_set, tuple):
486 return marginal_result_set
487 result, margin_keys, row_margin = marginal_result_set
488 else:
489 # no values, and table is a DataFrame
490 assert isinstance(table, ABCDataFrame)
491 marginal_result_set = _generate_marginal_results_without_values(
492 table, data, rows, cols, aggfunc, kwargs, observed, margins_name, dropna
493 )
494 if not isinstance(marginal_result_set, tuple):
495 return marginal_result_set
496 result, margin_keys, row_margin = marginal_result_set
497
498 # populate grand margin
499 for k in margin_keys:
500 if isinstance(k, str):
501 row_margin[k] = grand_margin[k]
502 else:
503 row_margin[k] = grand_margin[k[0]]
504
505 # GH#55484 recover the correct dtype when row_margin was initialized as
506 # object (len(cols)==0 path); no-op for already-typed series.
507 row_margin = row_margin.infer_objects()
508
509 from pandas import DataFrame
510
511 row_margin = row_margin.reindex(result.columns, fill_value=fill_value)
512 margin_dummy = DataFrame(row_margin, columns=Index([key])).T
513
514 for dtype in set(result.dtypes):
515 cols = result.select_dtypes([dtype]).columns
516 if isinstance(dtype, ExtensionDtype):
517 # GH#55484 margin_dummy may be object-dtype when row_margin was
518 # initialized with dtype=object (len(cols)==0 path); cast back.
519 margin_dummy[cols] = margin_dummy[cols].astype(dtype)
520 elif dtype != object and (margin_dummy[cols].dtypes == object).all():
521 # GH#55484 object-initialized row_margin can leave non-EA columns
522 # as object (mixed-values case); astype back to the target dtype.
523 margin_dummy[cols] = margin_dummy[cols].astype(dtype)
524 else:
525 margin_dummy[cols] = margin_dummy[cols].apply(
526 maybe_downcast_to_dtype, args=(dtype,)
527 )
528
529 row_names = result.index.names
530 result = concat([result, margin_dummy])
531 result.index.names = row_names
532
533 return result
534
535
536def _compute_grand_margin(
537 data: DataFrame, values, aggfunc, kwargs, margins_name: Hashable = "All"
538):
539 if values:
540 grand_margin = {}
541 for k, v in data[values].items():
542 try:
543 if isinstance(aggfunc, str):
544 grand_margin[k] = getattr(v, aggfunc)(**kwargs)
545 elif isinstance(aggfunc, dict):
546 if k in aggfunc:
547 if isinstance(aggfunc[k], str):
548 grand_margin[k] = getattr(v, aggfunc[k])(**kwargs)
549 else:
550 grand_margin[k] = aggfunc[k](v, **kwargs)
551 else:
552 grand_margin[k] = aggfunc(v, **kwargs)
553 except TypeError:
554 pass
555 return grand_margin
556 else:
557 from pandas import Categorical
558
559 # Use groupby for consistency with how values are aggregated.
560 gb = Series(np.nan, index=data.index).groupby(
561 Categorical(np.zeros(len(data.index)), categories=[0]), observed=False
562 )
563 agged = gb.agg(aggfunc, **kwargs)
564 assert isinstance(agged, Series) and len(agged) == 1
565 return {margins_name: agged.iloc[0]}
566
567
568def _generate_marginal_results(
569 table,
570 data: DataFrame,
571 values,
572 rows,
573 cols,
574 aggfunc,
575 kwargs,
576 observed: bool,
577 margins_name: Hashable = "All",
578 dropna: bool = True,
579):
580 margin_keys: list | Index
581 if len(cols) > 0:
582 # need to "interleave" the margins
583 table_pieces = []
584 margin_keys = []
585
586 def _all_key(key):
587 return (key, margins_name) + ("",) * (len(cols) - 1)
588
589 if len(rows) > 0:
590 margin = (
591 data[rows + values]
592 .groupby(rows, observed=observed, dropna=dropna)
593 .agg(aggfunc, **kwargs)
594 )
595 cat_axis = 1
596
597 for key, piece in table.T.groupby(level=0, observed=observed):
598 piece = piece.T
599 all_key = _all_key(key)
600
601 piece[all_key] = margin[key]
602
603 table_pieces.append(piece)
604 margin_keys.append(all_key)
605 else:
606 margin = (
607 data[cols[:1] + values]
608 .groupby(cols[:1], observed=observed, dropna=dropna)
609 .agg(aggfunc, **kwargs)
610 .T
611 )
612
613 cat_axis = 0
614 for key, piece in table.groupby(level=0, observed=observed):
615 if len(cols) > 1:
616 all_key = _all_key(key)
617 else:
618 all_key = margins_name
619 table_pieces.append(piece)
620 transformed_piece = margin[key].to_frame().T
621 if isinstance(piece.index, MultiIndex):
622 # We are adding an empty level
623 transformed_piece.index = MultiIndex.from_tuples(
624 [all_key],
625 names=[*piece.index.names, None],
626 )
627 else:
628 transformed_piece.index = Index([all_key], name=piece.index.name)
629
630 # append piece for margin into table_piece
631 table_pieces.append(transformed_piece)
632 margin_keys.append(all_key)
633
634 if not table_pieces:
635 # GH 49240
636 return table
637 else:
638 result = concat(table_pieces, axis=cat_axis)
639
640 if len(rows) == 0:
641 return result
642 else:
643 result = table
644 margin_keys = table.columns
645
646 if len(cols) > 0:
647 row_margin = (
648 data[cols + values]
649 .groupby(cols, observed=observed, dropna=dropna)
650 .agg(aggfunc, **kwargs)
651 )
652 row_margin = row_margin.stack()
653
654 # GH#26568. Use names instead of indices in case of numeric names
655 new_order_indices = itertools.chain([len(cols)], range(len(cols)))
656 new_order_names = [row_margin.index.names[i] for i in new_order_indices]
657 row_margin.index = row_margin.index.reorder_levels(new_order_names)
658 else:
659 # GH#55484 use object dtype so setitem works for grand-margin scalars
660 # whose dtype cannot hold NA (e.g. IntervalDtype with integer subtype);
661 # infer_objects is called in _add_margins after the values are set.
662 row_margin = data._constructor_sliced(index=result.columns, dtype=object)
663
664 return result, margin_keys, row_margin
665
666
667def _generate_marginal_results_without_values(
668 table: DataFrame,
669 data: DataFrame,
670 rows,
671 cols,
672 aggfunc,
673 kwargs,
674 observed: bool,
675 margins_name: Hashable = "All",
676 dropna: bool = True,
677):
678 margin_keys: list | Index
679 if len(cols) > 0:
680 # need to "interleave" the margins
681 margin_keys = []
682
683 def _all_key():
684 if len(cols) == 1:
685 return margins_name
686 return (margins_name,) + ("",) * (len(cols) - 1)
687
688 if len(rows) > 0:
689 if len(data.index) > 0:
690 gb = Series(np.nan, index=data.index).groupby(
691 [data[row] for row in rows], observed=observed, dropna=dropna
692 )
693 margin = gb.agg(aggfunc, **kwargs)
694 else:
695 from pandas import Categorical
696
697 gb = Series(np.nan, index=data.index).groupby(
698 Categorical(np.zeros(0), categories=[0]), observed=False
699 )
700 agged = gb.agg(aggfunc, **kwargs)
701 assert isinstance(agged, Series) and len(agged) == 1
702 margin = agged.iloc[0]
703 all_key = _all_key()
704 table[all_key] = margin
705 result = table
706 margin_keys.append(all_key)
707
708 else:
709 margin = data.groupby(level=0, observed=observed, dropna=dropna).agg(
710 aggfunc, **kwargs
711 )
712 all_key = _all_key()
713 table[all_key] = margin
714 result = table
715 margin_keys.append(all_key)
716 return result
717 else:
718 result = table
719 margin_keys = table.columns
720
721 if len(result.columns) > 0:
722 row_margin = (
723 Series(np.nan, index=data.index)
724 .groupby([data[col] for col in cols], observed=observed, dropna=dropna)
725 .agg(aggfunc, **kwargs)
726 )
727 else:
728 row_margin = Series(np.nan, index=result.columns)
729
730 return result, margin_keys, row_margin
731
732
733def _convert_by(by):
734 if by is None:
735 by = []
736 elif (
737 is_scalar(by)
738 or isinstance(by, (np.ndarray, Index, ABCSeries, Grouper))
739 or callable(by)
740 ):
741 by = [by]
742 else:
743 by = list(by)
744 return by
745
746
747@set_module("pandas")
748def pivot(
749 data: DataFrame,
750 *,
751 columns: IndexLabel,
752 index: IndexLabel | lib.NoDefault = lib.no_default,
753 values: IndexLabel | lib.NoDefault = lib.no_default,
754) -> DataFrame:
755 """
756 Return reshaped DataFrame organized by given index / column values.
757
758 Reshape data (produce a "pivot" table) based on column values. Uses
759 unique values from specified `index` / `columns` to form axes of the
760 resulting DataFrame. This function does not support data
761 aggregation, multiple values will result in a MultiIndex in the
762 columns. See the :ref:`User Guide <reshaping>` for more on reshaping.
763
764 Parameters
765 ----------
766 data : DataFrame
767 Input pandas DataFrame object.
768 columns : Hashable or a sequence of the previous
769 Column to use to make new frame's columns.
770 index : Hashable or a sequence of the previous, optional
771 Column to use to make new frame's index. If not given, uses existing index.
772 values : Hashable or a sequence of the previous, optional
773 Column(s) to use for populating new frame's values. If not
774 specified, all remaining columns will be used and the result will
775 have hierarchically indexed columns.
776
777 Returns
778 -------
779 DataFrame
780 Returns reshaped DataFrame.
781
782 Raises
783 ------
784 ValueError:
785 When there are any `index`, `columns` combinations with multiple
786 values. `DataFrame.pivot_table` when you need to aggregate.
787
788 See Also
789 --------
790 DataFrame.pivot_table : Generalization of pivot that can handle
791 duplicate values for one index/column pair.
792 DataFrame.unstack : Pivot based on the index values instead of a
793 column.
794 wide_to_long : Wide panel to long format. Less flexible but more
795 user-friendly than melt.
796
797 Notes
798 -----
799 For finer-tuned control, see hierarchical indexing documentation along
800 with the related stack/unstack methods.
801
802 Reference :ref:`the user guide <reshaping.pivot>` for more examples.
803
804 Examples
805 --------
806 >>> df = pd.DataFrame(
807 ... {
808 ... "foo": ["one", "one", "one", "two", "two", "two"],
809 ... "bar": ["A", "B", "C", "A", "B", "C"],
810 ... "baz": [1, 2, 3, 4, 5, 6],
811 ... "zoo": ["x", "y", "z", "q", "w", "t"],
812 ... }
813 ... )
814 >>> df
815 foo bar baz zoo
816 0 one A 1 x
817 1 one B 2 y
818 2 one C 3 z
819 3 two A 4 q
820 4 two B 5 w
821 5 two C 6 t
822
823 >>> df.pivot(index="foo", columns="bar", values="baz")
824 bar A B C
825 foo
826 one 1 2 3
827 two 4 5 6
828
829 >>> df.pivot(index="foo", columns="bar")["baz"]
830 bar A B C
831 foo
832 one 1 2 3
833 two 4 5 6
834
835 >>> df.pivot(index="foo", columns="bar", values=["baz", "zoo"])
836 baz zoo
837 bar A B C A B C
838 foo
839 one 1 2 3 x y z
840 two 4 5 6 q w t
841
842 You could also assign a list of column names or a list of index names.
843
844 >>> df = pd.DataFrame(
845 ... {
846 ... "lev1": [1, 1, 1, 2, 2, 2],
847 ... "lev2": [1, 1, 2, 1, 1, 2],
848 ... "lev3": [1, 2, 1, 2, 1, 2],
849 ... "lev4": [1, 2, 3, 4, 5, 6],
850 ... "values": [0, 1, 2, 3, 4, 5],
851 ... }
852 ... )
853 >>> df
854 lev1 lev2 lev3 lev4 values
855 0 1 1 1 1 0
856 1 1 1 2 2 1
857 2 1 2 1 3 2
858 3 2 1 2 4 3
859 4 2 1 1 5 4
860 5 2 2 2 6 5
861
862 >>> df.pivot(index="lev1", columns=["lev2", "lev3"], values="values")
863 lev2 1 2
864 lev3 1 2 1 2
865 lev1
866 1 0.0 1.0 2.0 NaN
867 2 4.0 3.0 NaN 5.0
868
869 >>> df.pivot(index=["lev1", "lev2"], columns=["lev3"], values="values")
870 lev3 1 2
871 lev1 lev2
872 1 1 0.0 1.0
873 2 2.0 NaN
874 2 1 4.0 3.0
875 2 NaN 5.0
876
877 A ValueError is raised if there are any duplicates.
878
879 >>> df = pd.DataFrame(
880 ... {
881 ... "foo": ["one", "one", "two", "two"],
882 ... "bar": ["A", "A", "B", "C"],
883 ... "baz": [1, 2, 3, 4],
884 ... }
885 ... )
886 >>> df
887 foo bar baz
888 0 one A 1
889 1 one A 2
890 2 two B 3
891 3 two C 4
892
893 Notice that the first two rows are the same for our `index`
894 and `columns` arguments.
895
896 >>> df.pivot(index="foo", columns="bar", values="baz")
897 Traceback (most recent call last):
898 ...
899 ValueError: Index contains duplicate entries, cannot reshape
900 """
901 columns_listlike = com.convert_to_list_like(columns)
902
903 # GH#35785 without this, downstream label arithmetic raises cryptically.
904 labels_to_check: list[tuple[str, list]] = [("columns", list(columns_listlike))]
905 if index is not lib.no_default:
906 labels_to_check.append(("index", list(com.convert_to_list_like(index))))
907 if values is not lib.no_default and not isinstance(values, tuple):
908 # GH#17160 a tuple ``values`` is a single (MultiIndex) label; the
909 # existing lookup already raises a KeyError naming it.
910 labels_to_check.append(("values", list(com.convert_to_list_like(values))))
911 for param_name, labels in labels_to_check:
912 missing = [label for label in labels if label not in data.columns]
913 if missing:
914 raise KeyError(
915 f"The following '{param_name}' labels are not columns of the "
916 f"DataFrame: {missing}"
917 )
918
919 # If columns is None we will create a MultiIndex level with None as name
920 # which might cause duplicated names because None is the default for
921 # level names
922 if any(name is None for name in data.index.names):
923 data = data.copy(deep=False)
924 data.index.names = [
925 name if name is not None else lib.no_default for name in data.index.names
926 ]
927
928 indexed: DataFrame | Series
929 if values is lib.no_default:
930 if index is not lib.no_default:
931 cols = com.convert_to_list_like(index)
932 else:
933 cols = []
934
935 append = index is lib.no_default
936 # error: Unsupported operand types for + ("List[Any]" and "ExtensionArray")
937 # error: Unsupported left operand type for + ("ExtensionArray")
938 indexed = data.set_index(
939 cols + columns_listlike, # type: ignore[operator]
940 append=append,
941 )
942 else:
943 index_list: list[Index] | list[Series]
944 if index is lib.no_default:
945 if isinstance(data.index, MultiIndex):
946 # GH 23955
947 index_list = [
948 data.index.get_level_values(i) for i in range(data.index.nlevels)
949 ]
950 else:
951 index_list = [
952 data._constructor_sliced(data.index, name=data.index.name)
953 ]
954 else:
955 index_list = [data[idx] for idx in com.convert_to_list_like(index)]
956
957 data_columns = [data[col] for col in columns_listlike]
958 index_list.extend(data_columns)
959 multiindex = MultiIndex.from_arrays(index_list)
960
961 if is_list_like(values) and not isinstance(values, tuple):
962 # Exclude tuple because it is seen as a single column name
963 indexed = data._constructor(
964 data[values]._values,
965 index=multiindex,
966 columns=cast("SequenceNotStr", values),
967 )
968 else:
969 indexed = data._constructor_sliced(data[values]._values, index=multiindex)
970 # error: Argument 1 to "unstack" of "DataFrame" has incompatible type "Union
971 # [List[Any], ExtensionArray, ndarray[Any, Any], Index, Series]"; expected
972 # "Hashable"
973 # unstack with a MultiIndex returns a DataFrame
974 result = cast("DataFrame", indexed.unstack(columns_listlike)) # type: ignore[arg-type]
975 result.index.names = [
976 name if name is not lib.no_default else None for name in result.index.names
977 ]
978
979 return result
980
981
982@set_module("pandas")
983def crosstab(
984 index,
985 columns,
986 values=None,
987 rownames=None,
988 colnames=None,
989 aggfunc=None,
990 margins: bool = False,
991 margins_name: Hashable = "All",
992 dropna: bool = True,
993 normalize: bool | Literal[0, 1, "all", "index", "columns"] = False,
994) -> DataFrame:
995 """
996 Compute a simple cross tabulation of two (or more) factors.
997
998 By default, computes a frequency table of the factors unless an
999 array of values and an aggregation function are passed.
1000
1001 Parameters
1002 ----------
1003 index : array-like, Series, or list of arrays/Series
1004 Values to group by in the rows.
1005 columns : array-like, Series, or list of arrays/Series
1006 Values to group by in the columns.
1007 values : array-like, optional
1008 Array of values to aggregate according to the factors.
1009 Requires `aggfunc` be specified.
1010 rownames : sequence, default None
1011 If passed, must match number of row arrays passed.
1012 colnames : sequence, default None
1013 If passed, must match number of column arrays passed.
1014 aggfunc : function, optional
1015 If specified, requires `values` be specified as well.
1016 margins : bool, default False
1017 Add row/column margins (subtotals).
1018 margins_name : str, default 'All'
1019 Name of the row/column that will contain the totals
1020 when margins is True.
1021 dropna : bool, default True
1022 Do not include columns whose entries are all NaN.
1023 normalize : bool, {'all', 'index', 'columns'}, or {0,1}, default False
1024 Normalize by dividing all values by the sum of values.
1025
1026 - If passed 'all' or `True`, will normalize over all values.
1027 - If passed 'index' will normalize over each row.
1028 - If passed 'columns' will normalize over each column.
1029 - If margins is `True`, will also normalize margin values.
1030
1031 Returns
1032 -------
1033 DataFrame
1034 Cross tabulation of the data.
1035
1036 See Also
1037 --------
1038 DataFrame.pivot : Reshape data based on column values.
1039 pivot_table : Create a pivot table as a DataFrame.
1040
1041 Notes
1042 -----
1043 Any Series passed will have their name attributes used unless row or column
1044 names for the cross-tabulation are specified.
1045
1046 With the default ``dropna=True``, categories without any observed
1047 instances in the input are dropped from the cross-tabulation. When
1048 ``dropna=False``, all categories of any Categorical input are included,
1049 even if the actual data does not contain any instances of a particular
1050 category.
1051
1052 Series arguments are aligned on their index before tabulating; rows where
1053 any value is missing after alignment (e.g. index labels not present in
1054 all Series) are dropped. In the event that there aren't overlapping
1055 indexes an empty DataFrame will be returned.
1056
1057 Reference :ref:`the user guide <reshaping.crosstabulations>` for more examples.
1058
1059 Examples
1060 --------
1061 >>> a = np.array(
1062 ... [
1063 ... "foo",
1064 ... "foo",
1065 ... "foo",
1066 ... "foo",
1067 ... "bar",
1068 ... "bar",
1069 ... "bar",
1070 ... "bar",
1071 ... "foo",
1072 ... "foo",
1073 ... "foo",
1074 ... ],
1075 ... dtype=object,
1076 ... )
1077 >>> b = np.array(
1078 ... [
1079 ... "one",
1080 ... "one",
1081 ... "one",
1082 ... "two",
1083 ... "one",
1084 ... "one",
1085 ... "one",
1086 ... "two",
1087 ... "two",
1088 ... "two",
1089 ... "one",
1090 ... ],
1091 ... dtype=object,
1092 ... )
1093 >>> c = np.array(
1094 ... [
1095 ... "dull",
1096 ... "dull",
1097 ... "shiny",
1098 ... "dull",
1099 ... "dull",
1100 ... "shiny",
1101 ... "shiny",
1102 ... "dull",
1103 ... "shiny",
1104 ... "shiny",
1105 ... "shiny",
1106 ... ],
1107 ... dtype=object,
1108 ... )
1109 >>> pd.crosstab(a, [b, c], rownames=["a"], colnames=["b", "c"])
1110 b one two
1111 c dull shiny dull shiny
1112 a
1113 bar 1 2 1 0
1114 foo 2 2 1 2
1115
1116 When `values` and `aggfunc` are passed, the values are aggregated within
1117 each group instead of counted:
1118
1119 >>> vals = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
1120 >>> pd.crosstab(a, b, values=vals, aggfunc="sum", rownames=["a"], colnames=["b"])
1121 b one two
1122 a
1123 bar 18 8
1124 foo 17 23
1125
1126 Here 'c' and 'f' are not represented in the data and will not be
1127 shown in the output because dropna is True by default. Set
1128 dropna=False to preserve categories with no data.
1129
1130 >>> foo = pd.Categorical(["a", "b"], categories=["a", "b", "c"])
1131 >>> bar = pd.Categorical(["d", "e"], categories=["d", "e", "f"])
1132 >>> pd.crosstab(foo, bar)
1133 col_0 d e
1134 row_0
1135 a 1 0
1136 b 0 1
1137 >>> pd.crosstab(foo, bar, dropna=False)
1138 col_0 d e f
1139 row_0
1140 a 1 0 0
1141 b 0 1 0
1142 c 0 0 0
1143 """
1144 if values is None and aggfunc is not None:
1145 raise ValueError("aggfunc cannot be used without values.")
1146
1147 if values is not None and aggfunc is None:
1148 raise ValueError("values cannot be used without an aggfunc.")
1149
1150 if not is_nested_list_like(index):
1151 index = [index]
1152 if not is_nested_list_like(columns):
1153 columns = [columns]
1154
1155 common_idx = None
1156 pass_objs = [x for x in index + columns if isinstance(x, (ABCSeries, ABCDataFrame))]
1157 if pass_objs:
1158 common_idx, _ = get_objs_combined_axis(pass_objs, intersect=True, sort=False)
1159
1160 rownames = _get_names(index, rownames, prefix="row")
1161 colnames = _get_names(columns, colnames, prefix="col")
1162
1163 # duplicate names mapped to unique names for pivot op
1164 (
1165 rownames_mapper,
1166 unique_rownames,
1167 colnames_mapper,
1168 unique_colnames,
1169 ) = _build_names_mapper(rownames, colnames)
1170
1171 from pandas import DataFrame
1172
1173 data = {
1174 **dict(zip(unique_rownames, index, strict=True)),
1175 **dict(zip(unique_colnames, columns, strict=True)),
1176 }
1177 df = DataFrame(data, index=common_idx)
1178
1179 if values is None:
1180 df["__dummy__"] = 0
1181 kwargs = {"aggfunc": len, "fill_value": 0}
1182 else:
1183 df["__dummy__"] = values
1184 kwargs = {"aggfunc": aggfunc}
1185
1186 # error: Argument 7 to "pivot_table" of "DataFrame" has incompatible type
1187 # "**Dict[str, object]"; expected "Union[...]"
1188 table = df.pivot_table(
1189 "__dummy__",
1190 index=unique_rownames,
1191 columns=unique_colnames,
1192 margins=margins,
1193 margins_name=margins_name,
1194 dropna=dropna,
1195 observed=dropna,
1196 **kwargs, # type: ignore[arg-type]
1197 )
1198
1199 # Post-process
1200 if normalize is not False:
1201 table = _normalize(
1202 table, normalize=normalize, margins=margins, margins_name=margins_name
1203 )
1204
1205 table = table.rename_axis(index=rownames_mapper, axis=0)
1206 table = table.rename_axis(columns=colnames_mapper, axis=1)
1207
1208 return table
1209
1210
1211def _normalize(
1212 table: DataFrame, normalize, margins: bool, margins_name: Hashable = "All"
1213) -> DataFrame:
1214 if not isinstance(normalize, (bool, str)):
1215 axis_subs = {0: "index", 1: "columns"}
1216 try:
1217 normalize = axis_subs[normalize]
1218 except KeyError as err:
1219 raise ValueError("Not a valid normalize argument") from err
1220
1221 if margins is False:
1222 # Actual Normalizations
1223 normalizers: dict[bool | str, Callable] = {
1224 "all": lambda x: x / x.sum(axis=1).sum(axis=0),
1225 "columns": lambda x: x / x.sum(),
1226 "index": lambda x: x.div(x.sum(axis=1), axis=0),
1227 }
1228
1229 normalizers[True] = normalizers["all"]
1230
1231 try:
1232 f = normalizers[normalize]
1233 except KeyError as err:
1234 raise ValueError("Not a valid normalize argument") from err
1235
1236 table = f(table)
1237 table = table.fillna(0)
1238
1239 elif margins is True:
1240 # keep index and column of pivoted table
1241 table_index = table.index
1242 table_columns = table.columns
1243 last_ind_or_col = table.iloc[-1, :].name
1244
1245 # check if margin name is not in (for MI cases) and not equal to last
1246 # index/column and save the column and index margin
1247 if (margins_name not in last_ind_or_col) & (margins_name != last_ind_or_col):
1248 raise ValueError(f"{margins_name} not in pivoted DataFrame")
1249 column_margin = table.iloc[:-1, -1]
1250 index_margin = table.iloc[-1, :-1]
1251
1252 # keep the core table
1253 table = table.iloc[:-1, :-1]
1254
1255 # Normalize core
1256 table = _normalize(table, normalize=normalize, margins=False)
1257
1258 # Fix Margins
1259 if normalize == "columns":
1260 column_margin = column_margin / column_margin.sum()
1261 table = concat([table, column_margin], axis=1)
1262 table = table.fillna(0)
1263 table.columns = table_columns
1264
1265 elif normalize == "index":
1266 index_margin = index_margin / index_margin.sum()
1267 table = table._append_internal(index_margin, ignore_index=True)
1268 table = table.fillna(0)
1269 table.index = table_index
1270
1271 elif normalize == "all" or normalize is True:
1272 column_margin = column_margin / column_margin.sum()
1273 index_margin = index_margin / index_margin.sum()
1274 margin_key: Hashable
1275 if isinstance(index_margin.index, MultiIndex):
1276 # GH#17024 expanding with a partial key is deprecated
1277 nlevels = index_margin.index.nlevels
1278 margin_key = (margins_name,) + ("",) * (nlevels - 1)
1279 else:
1280 margin_key = margins_name
1281 index_margin.loc[margin_key] = 1
1282 table = concat([table, column_margin], axis=1)
1283 table = table._append_internal(index_margin, ignore_index=True)
1284
1285 table = table.fillna(0)
1286 table.index = table_index
1287 table.columns = table_columns
1288
1289 else:
1290 raise ValueError("Not a valid normalize argument")
1291
1292 else:
1293 raise ValueError("Not a valid margins argument")
1294
1295 return table
1296
1297
1298def _get_names(arrs, names, prefix: str = "row") -> list:
1299 if names is None:
1300 names = []
1301 for i, arr in enumerate(arrs):
1302 if isinstance(arr, ABCSeries) and arr.name is not None:
1303 names.append(arr.name)
1304 else:
1305 names.append(f"{prefix}_{i}")
1306 else:
1307 if len(names) != len(arrs):
1308 raise AssertionError("arrays and names must have the same length")
1309 if not isinstance(names, list):
1310 names = list(names)
1311
1312 return names
1313
1314
1315def _build_names_mapper(
1316 rownames: list[str], colnames: list[str]
1317) -> tuple[dict[str, str], list[str], dict[str, str], list[str]]:
1318 """
1319 Given the names of a DataFrame's rows and columns, returns a set of unique row
1320 and column names and mappers that convert to original names.
1321
1322 A row or column name is replaced if it is duplicate among the rows of the inputs,
1323 among the columns of the inputs or between the rows and the columns.
1324
1325 Parameters
1326 ----------
1327 rownames: list[str]
1328 colnames: list[str]
1329
1330 Returns
1331 -------
1332 Tuple(Dict[str, str], List[str], Dict[str, str], List[str])
1333
1334 rownames_mapper: dict[str, str]
1335 a dictionary with new row names as keys and original rownames as values
1336 unique_rownames: list[str]
1337 a list of rownames with duplicate names replaced by dummy names
1338 colnames_mapper: dict[str, str]
1339 a dictionary with new column names as keys and original column names as values
1340 unique_colnames: list[str]
1341 a list of column names with duplicate names replaced by dummy names
1342
1343 """
1344 dup_names = set(rownames) | set(colnames)
1345
1346 rownames_mapper = {
1347 f"row_{i}": name for i, name in enumerate(rownames) if name in dup_names
1348 }
1349 unique_rownames = [
1350 f"row_{i}" if name in dup_names else name for i, name in enumerate(rownames)
1351 ]
1352
1353 colnames_mapper = {
1354 f"col_{i}": name for i, name in enumerate(colnames) if name in dup_names
1355 }
1356 unique_colnames = [
1357 f"col_{i}" if name in dup_names else name for i, name in enumerate(colnames)
1358 ]
1359
1360 return rownames_mapper, unique_rownames, colnames_mapper, unique_colnames