No processo de responder a essa pergunta para mim mesmo, aprendi muitas coisas e queria montar um catálogo de exemplos e algumas explicações.
A resposta específica para o ponto do levels
argumento virá no final.
pandas.concat
: The Missing Manual
Link para a documentação atual
Importação e definição de objetos
import pandas as pd
d1 = pd.DataFrame(dict(A=.1, B=.2, C=.3), index=[2, 3])
d2 = pd.DataFrame(dict(B=.4, C=.5, D=.6), index=[1, 2])
d3 = pd.DataFrame(dict(A=.7, B=.8, D=.9), index=[1, 3])
s1 = pd.Series([1, 2], index=[2, 3])
s2 = pd.Series([3, 4], index=[1, 2])
s3 = pd.Series([5, 6], index=[1, 3])
Argumentos
objs
O primeiro argumento que encontramos é objs
:
objs : uma sequência ou mapeamento de objetos Series, DataFrame ou Panel Se um dict for passado, as chaves classificadas serão usadas como o argumento keys, a menos que seja passado, caso em que os valores serão selecionados (veja abaixo). Qualquer objeto None será descartado silenciosamente a menos que sejam todos None, caso em que um ValueError será gerado
- Normalmente vemos isso usado com uma lista de objetos
Series
ou DataFrame
.
- Vou mostrar que também
dict
pode ser muito útil.
- Geradores também podem ser usados e podem ser úteis quando usados
map
como emmap(f, list_of_df)
Por enquanto, ficaremos com uma lista de alguns dos objetos DataFrame
e Series
definidos acima. Mostrarei como os dicionários podem ser aproveitados para fornecer MultiIndex
resultados muito úteis posteriormente.
pd.concat([d1, d2])
A B C D
2 0.1 0.2 0.3 NaN
3 0.1 0.2 0.3 NaN
1 NaN 0.4 0.5 0.6
2 NaN 0.4 0.5 0.6
axis
O segundo argumento que encontramos é axis
cujo valor padrão é 0
:
eixo : {0 / 'índice', 1 / 'colunas'}, padrão 0 O eixo ao longo do qual concatenar.
Dois DataFrame
s com axis=0
(empilhados)
Para valores de 0
ou index
queremos dizer: "Alinhe ao longo das colunas e adicione ao índice".
Conforme mostrado acima, onde usamos axis=0
, porque 0
é o valor padrão, e vemos que o índice de d2
estende o índice de d1
apesar de haver sobreposição do valor 2
:
pd.concat([d1, d2], axis=0)
A B C D
2 0.1 0.2 0.3 NaN
3 0.1 0.2 0.3 NaN
1 NaN 0.4 0.5 0.6
2 NaN 0.4 0.5 0.6
Dois DataFrame
s com axis=1
(lado a lado)
Para valores 1
ou columns
queremos dizer: "Alinhe ao longo do índice e adicione às colunas",
pd.concat([d1, d2], axis=1)
A B C B C D
1 NaN NaN NaN 0.4 0.5 0.6
2 0.1 0.2 0.3 0.4 0.5 0.6
3 0.1 0.2 0.3 NaN NaN NaN
Podemos ver que o índice resultante é a união dos índices e as colunas resultantes são a extensão das colunas de d1
pelas colunas de d2
.
Dois (ou três) Series
com axis=0
(empilhados)
Ao combinar pandas.Series
junto axis=0
, obtemos a pandas.Series
. O nome do resultado Series
será a None
menos que todos Series
sendo combinados tenham o mesmo nome. Preste atenção ao 'Name: A'
quando imprimirmos o resultado Series
. Quando não está presente, podemos assumir que o Series
nome está None
.
| | | pd.concat(
| pd.concat( | pd.concat( | [s1.rename('A'),
pd.concat( | [s1.rename('A'), | [s1.rename('A'), | s2.rename('B'),
[s1, s2]) | s2]) | s2.rename('A')]) | s3.rename('A')])
-------------- | --------------------- | ---------------------- | ----------------------
2 1 | 2 1 | 2 1 | 2 1
3 2 | 3 2 | 3 2 | 3 2
1 3 | 1 3 | 1 3 | 1 3
2 4 | 2 4 | 2 4 | 2 4
dtype: int64 | dtype: int64 | Name: A, dtype: int64 | 1 5
| | | 3 6
| | | dtype: int64
Dois (ou três) Series
com axis=1
(lado a lado)
Ao combinar pandas.Series
junto axis=1
, é o name
atributo ao qual nos referimos para inferir um nome de coluna no resultado pandas.DataFrame
.
| | pd.concat(
| pd.concat( | [s1.rename('X'),
pd.concat( | [s1.rename('X'), | s2.rename('Y'),
[s1, s2], axis=1) | s2], axis=1) | s3.rename('Z')], axis=1)
---------------------- | --------------------- | ------------------------------
0 1 | X 0 | X Y Z
1 NaN 3.0 | 1 NaN 3.0 | 1 NaN 3.0 5.0
2 1.0 4.0 | 2 1.0 4.0 | 2 1.0 4.0 NaN
3 2.0 NaN | 3 2.0 NaN | 3 2.0 NaN 6.0
Misturado Series
e DataFrame
com axis=0
(empilhado)
Ao realizar uma concatenação de a Series
e DataFrame
along axis=0
, convertemos todos Series
em uma única coluna DataFrame
s.
Observe que esta é uma concatenação junto axis=0
; isso significa estender o índice (linhas) enquanto alinha as colunas. Nos exemplos abaixo, vemos que o índice se torna, [2, 3, 2, 3]
que é um acréscimo indiscriminado de índices. As colunas não se sobrepõem, a menos que eu force a nomenclatura da Series
coluna com o argumento para to_frame
:
pd.concat( |
[s1.to_frame(), d1]) | pd.concat([s1, d1])
------------------------- | ---------------------
0 A B C | 0 A B C
2 1.0 NaN NaN NaN | 2 1.0 NaN NaN NaN
3 2.0 NaN NaN NaN | 3 2.0 NaN NaN NaN
2 NaN 0.1 0.2 0.3 | 2 NaN 0.1 0.2 0.3
3 NaN 0.1 0.2 0.3 | 3 NaN 0.1 0.2 0.3
Você pode ver que os resultados pd.concat([s1, d1])
são os mesmos como se eu tivesse executado a to_frame
mim mesmo.
No entanto, posso controlar o nome da coluna resultante com um parâmetro para to_frame
. Renomear o Series
com o rename
método não controla o nome da coluna no resultado DataFrame
.
# Effectively renames | |
# `s1` but does not align | # Does not rename. So | # Renames to something
# with columns in `d1` | # Pandas defaults to `0` | # that does align with `d1`
pd.concat( | pd.concat( | pd.concat(
[s1.to_frame('X'), d1]) | [s1.rename('X'), d1]) | [s1.to_frame('B'), d1])
---------------------------- | -------------------------- | ----------------------------
A B C X | 0 A B C | A B C
2 NaN NaN NaN 1.0 | 2 1.0 NaN NaN NaN | 2 NaN 1.0 NaN
3 NaN NaN NaN 2.0 | 3 2.0 NaN NaN NaN | 3 NaN 2.0 NaN
2 0.1 0.2 0.3 NaN | 2 NaN 0.1 0.2 0.3 | 2 0.1 0.2 0.3
3 0.1 0.2 0.3 NaN | 3 NaN 0.1 0.2 0.3 | 3 0.1 0.2 0.3
Misturado Series
e DataFrame
com axis=1
(lado a lado)
Isso é bastante intuitivo. Series
O nome da coluna é padronizado para uma enumeração de tais Series
objetos quando um name
atributo não está disponível.
| pd.concat(
pd.concat( | [s1.rename('X'),
[s1, d1], | s2, s3, d1],
axis=1) | axis=1)
------------------- | -------------------------------
0 A B C | X 0 1 A B C
2 1 0.1 0.2 0.3 | 1 NaN 3.0 5.0 NaN NaN NaN
3 2 0.1 0.2 0.3 | 2 1.0 4.0 NaN 0.1 0.2 0.3
| 3 2.0 NaN 6.0 0.1 0.2 0.3
join
O terceiro argumento é join
que descreve se a mesclagem resultante deve ser uma mesclagem externa (padrão) ou uma mesclagem interna.
join : {'interno', 'externo'}, padrão 'externo'
Como lidar com índices em outro (s) eixo (s).
Acontece que não há nenhuma opção left
ou right
que pd.concat
possa lidar com mais do que apenas dois objetos para mesclar.
No caso de d1
e d2
, as opções são semelhantes a:
outer
pd.concat([d1, d2], axis=1, join='outer')
A B C B C D
1 NaN NaN NaN 0.4 0.5 0.6
2 0.1 0.2 0.3 0.4 0.5 0.6
3 0.1 0.2 0.3 NaN NaN NaN
inner
pd.concat([d1, d2], axis=1, join='inner')
A B C B C D
2 0.1 0.2 0.3 0.4 0.5 0.6
join_axes
O quarto argumento é o que nos permite fazer nossa left
fusão e muito mais.
join_axes : lista de objetos Index
Índices específicos a serem usados para os outros n - 1 eixos em vez de realizar a lógica de conjunto interno / externo.
Esquerda Unir
pd.concat([d1, d2, d3], axis=1, join_axes=[d1.index])
A B C B C D A B D
2 0.1 0.2 0.3 0.4 0.5 0.6 NaN NaN NaN
3 0.1 0.2 0.3 NaN NaN NaN 0.7 0.8 0.9
Unir à direita
pd.concat([d1, d2, d3], axis=1, join_axes=[d3.index])
A B C B C D A B D
1 NaN NaN NaN 0.4 0.5 0.6 0.7 0.8 0.9
3 0.1 0.2 0.3 NaN NaN NaN 0.7 0.8 0.9
ignore_index
ignore_index : boolean, default False
Se True, não use os valores de índice ao longo do eixo de concatenação. O eixo resultante será rotulado 0, ..., n - 1. Isso é útil se você estiver concatenando objetos onde o eixo de concatenação não tem informações de indexação significativas. Observe que os valores de índice nos outros eixos ainda são respeitados na junção.
Como quando eu empilhar d1
em cima do d2
, se eu não me importar com os valores do índice, posso redefini-los ou ignorá-los.
| pd.concat( | pd.concat(
| [d1, d2], | [d1, d2]
pd.concat([d1, d2]) | ignore_index=True) | ).reset_index(drop=True)
--------------------- | ----------------------- | -------------------------
A B C D | A B C D | A B C D
2 0.1 0.2 0.3 NaN | 0 0.1 0.2 0.3 NaN | 0 0.1 0.2 0.3 NaN
3 0.1 0.2 0.3 NaN | 1 0.1 0.2 0.3 NaN | 1 0.1 0.2 0.3 NaN
1 NaN 0.4 0.5 0.6 | 2 NaN 0.4 0.5 0.6 | 2 NaN 0.4 0.5 0.6
2 NaN 0.4 0.5 0.6 | 3 NaN 0.4 0.5 0.6 | 3 NaN 0.4 0.5 0.6
E ao usar axis=1
:
| pd.concat(
| [d1, d2], axis=1,
pd.concat([d1, d2], axis=1) | ignore_index=True)
------------------------------- | -------------------------------
A B C B C D | 0 1 2 3 4 5
1 NaN NaN NaN 0.4 0.5 0.6 | 1 NaN NaN NaN 0.4 0.5 0.6
2 0.1 0.2 0.3 0.4 0.5 0.6 | 2 0.1 0.2 0.3 0.4 0.5 0.6
3 0.1 0.2 0.3 NaN NaN NaN | 3 0.1 0.2 0.3 NaN NaN NaN
keys
Podemos passar uma lista de valores escalares ou tuplas para atribuir tupla ou valores escalares ao MultiIndex correspondente. O comprimento da lista passada deve ser igual ao número de itens sendo concatenados.
keys : sequence, default None
Se vários níveis forem passados, deve conter tuplas. Construa um índice hierárquico usando as chaves passadas como o nível mais externo
axis=0
Ao concatenar Series
objetos ao longo axis=0
(estendendo o índice).
Essas chaves se tornam um novo nível inicial de um MultiIndex
objeto no atributo de índice.
# length 3 length 3 # length 2 length 2
# /--------\ /-----------\ # /----\ /------\
pd.concat([s1, s2, s3], keys=['A', 'B', 'C']) pd.concat([s1, s2], keys=['A', 'B'])
---------------------------------------------- -------------------------------------
A 2 1 A 2 1
3 2 3 2
B 1 3 B 1 3
2 4 2 4
C 1 5 dtype: int64
3 6
dtype: int64
No entanto, podemos usar mais do que valores escalares no keys
argumento para criar um ainda mais profundo MultiIndex
. Aqui, passamos tuples
do comprimento 2 antes de dois novos níveis de um MultiIndex
:
pd.concat(
[s1, s2, s3],
keys=[('A', 'X'), ('A', 'Y'), ('B', 'X')])
-----------------------------------------------
A X 2 1
3 2
Y 1 3
2 4
B X 1 5
3 6
dtype: int64
axis=1
É um pouco diferente quando se estende ao longo de colunas. Quando usamos axis=0
(veja acima) nossos níveis keys
agiram como MultiIndex
adicionais ao índice existente. Pois axis=1
, estamos nos referindo a um eixo que os Series
objetos não têm, ou seja, o columns
atributo.
Variações de dois
Series
com
axis=1
Observe que nomear o s1
e é s2
importante, desde que não keys
seja passado, mas será substituído se keys
for aprovado.
| | | pd.concat(
| pd.concat( | pd.concat( | [s1.rename('U'),
pd.concat( | [s1, s2], | [s1.rename('U'), | s2.rename('V')],
[s1, s2], | axis=1, | s2.rename('V')], | axis=1,
axis=1) | keys=['X', 'Y']) | axis=1) | keys=['X', 'Y'])
-------------- | --------------------- | ---------------------- | ----------------------
0 1 | X Y | U V | X Y
1 NaN 3.0 | 1 NaN 3.0 | 1 NaN 3.0 | 1 NaN 3.0
2 1.0 4.0 | 2 1.0 4.0 | 2 1.0 4.0 | 2 1.0 4.0
3 2.0 NaN | 3 2.0 NaN | 3 2.0 NaN | 3 2.0 NaN
MultiIndex
com
Series
e
axis=1
pd.concat(
[s1, s2],
axis=1,
keys=[('W', 'X'), ('W', 'Y')])
-----------------------------------
W
X Y
1 NaN 3.0
2 1.0 4.0
3 2.0 NaN
Dois
DataFrame
com
axis=1
Como nos axis=0
exemplos, keys
adicione níveis a a MultiIndex
, mas desta vez ao objeto armazenado no columns
atributo.
pd.concat( | pd.concat(
[d1, d2], | [d1, d2],
axis=1, | axis=1,
keys=['X', 'Y']) | keys=[('First', 'X'), ('Second', 'X')])
------------------------------- | --------------------------------------------
X Y | First Second
A B C B C D | X X
1 NaN NaN NaN 0.4 0.5 0.6 | A B C B C D
2 0.1 0.2 0.3 0.4 0.5 0.6 | 1 NaN NaN NaN 0.4 0.5 0.6
3 0.1 0.2 0.3 NaN NaN NaN | 2 0.1 0.2 0.3 0.4 0.5 0.6
| 3 0.1 0.2 0.3 NaN NaN NaN
Series
e
DataFrame
com
axis=1
Isso é complicado. Nesse caso, um valor de chave escalar não pode atuar como o único nível de índice para o Series
objeto quando ele se torna uma coluna, ao mesmo tempo que atua como o primeiro nível de a MultiIndex
para o DataFrame
. Portanto, o Pandas usará novamente o name
atributo do Series
objeto como a origem do nome da coluna.
pd.concat( | pd.concat(
[s1, d1], | [s1.rename('Z'), d1],
axis=1, | axis=1,
keys=['X', 'Y']) | keys=['X', 'Y'])
--------------------- | --------------------------
X Y | X Y
0 A B C | Z A B C
2 1 0.1 0.2 0.3 | 2 1 0.1 0.2 0.3
3 2 0.1 0.2 0.3 | 3 2 0.1 0.2 0.3
Limitações
keys
e
MultiIndex
inferências.
O Pandas parece apenas inferir nomes de colunas a partir do Series
nome, mas não preencherá os espaços em branco ao fazer uma concatenação análoga entre quadros de dados com um número diferente de níveis de coluna.
d1_ = pd.concat(
[d1], axis=1,
keys=['One'])
d1_
One
A B C
2 0.1 0.2 0.3
3 0.1 0.2 0.3
Em seguida, concatene isso com outro quadro de dados com apenas um nível no objeto colunas e Pandas se recusará a tentar fazer tuplas do MultiIndex
objeto e combinar todos os quadros de dados como se fosse um único nível de objetos, escalares e tuplas.
pd.concat([d1_, d2], axis=1)
(One, A) (One, B) (One, C) B C D
1 NaN NaN NaN 0.4 0.5 0.6
2 0.1 0.2 0.3 0.4 0.5 0.6
3 0.1 0.2 0.3 NaN NaN NaN
Passando por um em dict
vez de umlist
Ao passar um dicionário, pandas.concat
usará as chaves do dicionário como keys
parâmetro.
# axis=0 | # axis=1
pd.concat( | pd.concat(
{0: d1, 1: d2}) | {0: d1, 1: d2}, axis=1)
----------------------- | -------------------------------
A B C D | 0 1
0 2 0.1 0.2 0.3 NaN | A B C B C D
3 0.1 0.2 0.3 NaN | 1 NaN NaN NaN 0.4 0.5 0.6
1 1 NaN 0.4 0.5 0.6 | 2 0.1 0.2 0.3 0.4 0.5 0.6
2 NaN 0.4 0.5 0.6 | 3 0.1 0.2 0.3 NaN NaN NaN
levels
Isso é usado em conjunto com o keys
argumento. Quando levels
for deixado como seu valor padrão de None
, o Pandas pegará os valores exclusivos de cada nível do resultante MultiIndex
e os usará como o objeto usado no index.levels
atributo resultante .
níveis : lista de sequências, padrão Nenhum
Níveis específicos (valores exclusivos) para usar para construir um MultiIndex. Caso contrário, eles serão inferidos das chaves.
Se o Pandas já infere quais deveriam ser esses níveis, qual a vantagem de especificá-los nós mesmos? Vou mostrar um exemplo e deixar para você pensar em outras razões pelas quais isso pode ser útil.
Exemplo
De acordo com a documentação, o levels
argumento é uma lista de sequências. Isso significa que podemos usar outro pandas.Index
como uma dessas sequências.
Considere o quadro de dados df
que é a concatenação de d1
, d2
e d3
:
df = pd.concat(
[d1, d2, d3], axis=1,
keys=['First', 'Second', 'Fourth'])
df
First Second Fourth
A B C B C D A B D
1 NaN NaN NaN 0.4 0.5 0.6 0.7 0.8 0.9
2 0.1 0.2 0.3 0.4 0.5 0.6 NaN NaN NaN
3 0.1 0.2 0.3 NaN NaN NaN 0.7 0.8 0.9
Os níveis do objeto colunas são:
print(df, *df.columns.levels, sep='\n')
Index(['First', 'Second', 'Fourth'], dtype='object')
Index(['A', 'B', 'C', 'D'], dtype='object')
Se usarmos sum
em um groupby
, obtemos:
df.groupby(axis=1, level=0).sum()
First Fourth Second
1 0.0 2.4 1.5
2 0.6 0.0 1.5
3 0.6 2.4 0.0
Mas e se, em vez de, ['First', 'Second', 'Fourth']
houvesse outra categoria ausente chamada Third
e Fifth
? E eu queria que eles fossem incluídos nos resultados de uma groupby
agregação? Podemos fazer isso se tivermos um pandas.CategoricalIndex
. E podemos especificar isso com antecedência com o levels
argumento.
Então, em vez disso, vamos definir df
como:
cats = ['First', 'Second', 'Third', 'Fourth', 'Fifth']
lvl = pd.CategoricalIndex(cats, categories=cats, ordered=True)
df = pd.concat(
[d1, d2, d3], axis=1,
keys=['First', 'Second', 'Fourth'],
levels=[lvl]
)
df
First Fourth Second
1 0.0 2.4 1.5
2 0.6 0.0 1.5
3 0.6 2.4 0.0
Mas o primeiro nível do objeto de colunas é:
df.columns.levels[0]
CategoricalIndex(
['First', 'Second', 'Third', 'Fourth', 'Fifth'],
categories=['First', 'Second', 'Third', 'Fourth', 'Fifth'],
ordered=True, dtype='category')
E nosso groupby
resumo se parece com:
df.groupby(axis=1, level=0).sum()
First Second Third Fourth Fifth
1 0.0 1.5 0.0 2.4 0.0
2 0.6 1.5 0.0 0.0 0.0
3 0.6 0.0 0.0 2.4 0.0
names
Isso é usado para nomear os níveis de um resultante MultiIndex
. O comprimento da names
lista deve corresponder ao número de níveis no resultado MultiIndex
.
nomes : lista, padrão Nenhum
Nomes para os níveis no índice hierárquico resultante
# axis=0 | # axis=1
pd.concat( | pd.concat(
[d1, d2], | [d1, d2],
keys=[0, 1], | axis=1, keys=[0, 1],
names=['lvl0', 'lvl1']) | names=['lvl0', 'lvl1'])
----------------------------- | ----------------------------------
A B C D | lvl0 0 1
lvl0 lvl1 | lvl1 A B C B C D
0 2 0.1 0.2 0.3 NaN | 1 NaN NaN NaN 0.4 0.5 0.6
3 0.1 0.2 0.3 NaN | 2 0.1 0.2 0.3 0.4 0.5 0.6
1 1 NaN 0.4 0.5 0.6 | 3 0.1 0.2 0.3 NaN NaN NaN
2 NaN 0.4 0.5 0.6 |
verify_integrity
Documentação autoexplicativa
verify_integrity : boolean, default False
Verifique se o novo eixo concatenado contém duplicatas. Isso pode ser muito caro em relação à concatenação de dados real.
Como o índice resultante da concatenação d1
e d2
não é exclusivo, ele falhará na verificação de integridade.
pd.concat([d1, d2])
A B C D
2 0.1 0.2 0.3 NaN
3 0.1 0.2 0.3 NaN
1 NaN 0.4 0.5 0.6
2 NaN 0.4 0.5 0.6
E
pd.concat([d1, d2], verify_integrity=True)
> ValueError: os índices têm valores sobrepostos: [2]
pd.concat(..., levels=[lvl]).groupby(axis=1, level=0).sum()
produz um resultado diferente depd.concat(..., levels=[cats]).groupby(axis=1, level=0).sum()
. Você sabe por quê? Os documentos dizem apenas quelevels
deve haver uma lista de sequências.Passing a dict instead of a list
precisa de um exemplo usando um dicionário, não uma lista.dict
exemplo, thx. A razão é quelvl
é um índice categórico ecats
é apenas uma lista. Ao agrupar por um tipo categórico, as categorias ausentes são preenchidas com zeros e nulos quando apropriado. Veja isto