Skip to content

Campo do modelo que o Arrow não tipa sai como Object na saída polars - #220

Merged
bdcdo merged 5 commits into
mainfrom
fix/polars-coluna-nao-tipavel
Oct 13, 2026
Merged

bdcdo merged 5 commits into
mainfrom
fix/polars-coluna-nao-tipavel

Conversation

@bdcdo

@bdcdo bdcdo commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

O quê

Com entrada polars, a conversão final dos campos do modelo falhava depois de todas as chamadas ao provider quando as respostas não cabiam numa coluna tipada. Um campo Union[int, str] que recebe 1 numa linha e "dois" em outra levantava ArrowInvalid: Could not convert 'dois' with type str: tried to convert to int64, e sem checkpoint_path as respostas se perdiam. Com entrada pandas, o mesmo modelo devolve a coluna object.

A falha atinge nove formas de campo: Enum com valores em texto, tuple[int, str], Union[int, str], Any e dict[str, Any] com tipos diferentes, list[Union[int, str]], Union[int, float] com inteiro acima de 2**53 ao lado de float, int acima do Int64 (OverflowError) e datetime com fuso de deslocamento, como +05:30 (ComputeError).

Como

  • A conversão passa a ser coluna a coluna. A coluna que o polars ou o pyarrow recusam, seja qual for a exceção, sai com o tipo Object, com os objetos Python que a entrada pandas devolveria, e um UserWarning único nomeia essas colunas e avisa que precisam de conversão antes de ir para arquivo.
  • O campo Enum sai pelo valor do membro, como o checkpoint já grava, inclusive na coluna Object. Dentro de lista ou dict o Enum fica como está, e a coluna sai Object.
  • A coluna Object é montada por map_elements, que guarda o que a função devolve. O construtor de Series inspeciona os valores mesmo com dtype=pl.Object: quando o primeiro valor não nulo é membro de Enum, troca cada valor pelo .value e levanta erro no que não o tem.
  • O extra polars passa a exigir polars>=1.0. No 0.20 há versões que convertem a coluna de texto com um valor de outro tipo pondo nulo no lugar dele, sem erro, e versões em que o to_pandas() de uma coluna Object, pelo qual ela volta na retomada, devolve bytes ou entra em pânico. Suportá-las pedia uma comparação de nulos e uma releitura da coluna que só o job de versões mínimas exercitava.
  • A remoção das colunas de status ociosas sai de from_pandas para _drop_idle_control_columns, sem mudança de comportamento, para a função caber no limite de complexidade do lint.
  • A referência (docs/reference/api.md e a versão em inglês) descreve o retorno com entrada polars.

Testes

  • Um caso por forma de campo, de ponta a ponta: com entrada polars a coluna sai Object com os mesmos valores da entrada pandas, e o aviso nomeia a coluna.
  • O aviso é um só para todas as colunas recusadas, nomeia só elas e aponta para a linha que chamou dataframeit(); o Enum sai pelo valor, com e sem coluna Object; o membro de Enum que chega à coluna Object fica inteiro; entrada polars.Series; retomada de uma saída com coluna Object; a coluna com nulo legítimo (NaN, pd.NA, NaT, None, lista com None, toda nula) continua tipada e sem aviso.
  • Suíte completa: Python 3.13 (1184 passam) e 3.10 (1179) com todos os extras, job do codex (1175 e 1170), cobertura de ramos combinada em 100%, e versões mínimas com polars 1.0.0, pandas 2.1.2 e pyarrow 10 (1178 passam). ruff, ty, vulture, deptry e mkdocs build passam.

Closes #218

Com entrada polars, a conversão final dos campos do modelo levantava
ArrowInvalid, TypeError ou OverflowError depois de todas as chamadas
quando as respostas não cabiam numa coluna tipada: Union[int, str] com
número numa linha e texto em outra, tuple[int, str], Any e dict[str, Any]
com tipos diferentes, lista de Enum, inteiro acima de 2**53 ao lado de
float e inteiro acima do Int64. Sem checkpoint_path, as respostas se
perdiam.

A conversão passa a ser coluna a coluna. O Enum vai pelo valor, como no
checkpoint. A coluna que o Arrow recusa sai com o tipo Object, com os
objetos Python que a entrada pandas devolveria, e um aviso a nomeia.

Na ida, a coluna Object é lida pelos objetos, para que a saída com uma
coluna dessas possa ser retomada: no polars 0.20, o to_pandas() devolve
bytes no lugar deles.

A remoção das colunas de status ociosas sai de from_pandas para uma
função própria.

Closes #218
@bdcdo

bdcdo commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Revisão independente: achados abertos, PR ainda não pronto para merge

Um subagente novo revisou fe627503 em worktrees descartáveis, com suíte, mutações e comparação contra a main em polars 0.20.0, 0.20.31, 1.0.0, 1.20.0 e 2.0.0. Nas três configurações que o CI roda não achou regressão de tipo nem de valor. Os achados abaixo estão abertos; os três primeiros foram reproduzidos de novo fora do revisor.

1. Polars 0.20.23 a 0.20.31: valor trocado por nulo sem erro. Nessas versões, pl.from_pandas de uma Series de objetos que começa com texto não levanta e devolve nulo no lugar do que não é texto: pl.from_pandas(pd.Series(["dois", 1], dtype=object)) dá String ['dois', None]. Na conversão por coluna deste PR a coluna sai tipada, sem aviso e sem o valor; na main o frame inteiro ia pelo Arrow e levantava. As versões 0.20.0 a 0.20.22 levantam ArrowTypeError, e da 1.0.0 em diante, TypeError. Correção proposta: depois de converter, cair no ramo Object quando a contagem de nulos for maior que a de values.isna().

2. Erros que escapam da captura e ainda derrubam a saída depois das chamadas. polars.exceptions.ComputeError não é ValueError nem TypeError. Um campo datetime com fuso +05:30 levanta ComputeError: unable to parse time zone em todas as versões testadas, e no 0.20.0 um dict[str, int] com {} em todas as linhas também. A main falha igual; é a mesma classe de falha da issue 218. Proposta: capturar Exception nesse ponto, com a razão escrita. PanicException (um Decimal de 40 dígitos) continua fora do alcance.

3. A construção da coluna Object pode falhar ela mesma. pl.Series(col, lista, dtype=pl.Object) inspeciona os valores: no polars 2.0.0, [Cor.AZUL, None, 2] levanta AttributeError: 'NoneType' object has no attribute 'value', e no 0.20.0 o mesmo acontece com range. Construída a partir de um array numpy de objetos, a Series guarda os objetos intactos nas quatro versões testadas. Com isso o ramo Object pode receber os valores como o pandas os guarda, com o Enum como membro.

4. Testes. Três mutações passam: stacklevel=4 (o teste e _rodar estão no mesmo arquivo), um aviso por coluna em vez de um só (nenhum teste tem duas colunas recusadas) e o ramo Object usando os valores de antes da troca do Enum.

5. Texto. O aviso e a referência mandam converter "antes de gravar em parquet", mas CSV, IPC e JSON também recusam coluna Object. A frase da referência sobre colunas de entrada não vale para coluna com nome de família de controle, como _trace_x. Das 233 diferenças contra a main citadas no corpo, 184 são campo Enum saindo pelo valor e 49 são coluna Object.

Aceito como está. Union[str, bytes] com ["a", b"b", "c"] saía String, com o bytes decodificado sem aviso, e passa a sair Object com aviso, que é o que a entrada pandas devolve. Com avisos tratados como erro o resultado se perde, como em qualquer outro aviso de dataframeit().

bdcdo added 2 commits October 13, 2026 12:59
… a saída

Com entrada polars, três casos ainda escapavam da degradação para Object:

- O polars 0.20.23 a 0.20.31 converte a coluna de texto com um número e
  põe nulo no lugar dele, sem levantar erro. A coluna que volta com mais
  nulos do que tinha no pandas passa a sair Object. A Struct fica fora da
  comparação, porque o polars 0.20 e o 1.0 contam como nula a linha com
  todos os campos nulos.
- O ComputeError do polars, como o do datetime com fuso +05:30, não é
  ValueError nem TypeError e derrubava a saída depois de todas as
  chamadas. Qualquer exceção da conversão leva a coluna a Object.
- A coluna Object é montada por map_elements. O pl.Series com
  dtype=pl.Object olha o primeiro valor e levanta erro com range e
  dataclass no polars 0.20.0, e o array numpy de objetos que começa por
  texto não sai Object do 0.20.22 em diante.

O Enum sai pelo valor também na coluna Object. O aviso e a referência
falam em gravar em arquivo, porque CSV, IPC e JSON também recusam a
coluna Object, e a referência diz que a coluna de entrada com nome de
coluna adicionada não volta como entrou.

Os testes novos conferem a linha para a qual o aviso aponta, o aviso
único para duas colunas, o Enum em coluna Object, o nulo a mais (simulado,
para não depender da versão instalada) e as colunas com nulo que
continuam tipadas.
@bdcdo

bdcdo commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Segunda revisão independente: sem achado que bloqueie; piso do polars sobe para 1.0

Os achados da primeira revisão foram corrigidos em 5d3d4c6c. Um subagente novo revisou esse commit em worktrees descartáveis: 80 tipos de campo por 4 cenários (execução completa, com linha em erro, retomada com 1 e com 2 linhas pendentes), main contra branch, em 9 combinações de versão; 75 colunas com nulo legítimo direto em from_pandas; 17 mutações em cada ambiente; e todos os jobs do CI reproduzidos.

O que confirmou. Nenhuma regressão contra a main: toda diferença é a main levantando erro ou, no polars 0.20.23 e 0.20.31, trocando valor por nulo sem aviso. Nenhum falso positivo da comparação de nulos e nenhum valor perdido sem aviso. Suíte verde de ponta a ponta no polars 0.20.0, 0.20.22, 0.20.23, 0.20.31, 1.0.0, 1.24.0 e 2.0.0; cobertura de ramos combinada em 100%; ruff, ty, vulture, deptry e mkdocs build limpos.

Achados, nenhum bloqueia:

  1. No polars 0.20.2 a 0.20.5, o to_pandas() entra em pânico com coluna Object, e a retomada de uma saída com coluna Object levanta PanicException. A main não chega a esse ponto, porque não produz a coluna.
  2. O dtype=object da releitura da coluna Object não tem teste que o prenda.
  3. O aviso e a referência dizem que a coluna Object traz os objetos "como o modelo os devolveu", o que não vale para Enum, que sai pelo valor.
  4. O comentário sobre o array numpy só vale do polars 0.20.10 em diante.
  5. Coluna de nome não textual no ramo Object levanta TypeError. Só se alcança chamando from_pandas direto, e na main o mesmo caso já levantava; fica como está.

A revisão também mediu que três proteções do diff (a comparação de nulos com a exceção de Struct, a releitura da coluna Object por to_list() e parte dos contornos do construtor de Series) só existem por versões anteriores à 1.2 e só são exercidas pelo job de versões mínimas.

Decisão do dono do repositório: o extra polars passa a exigir polars>=1.0. Com isso saem a releitura por to_list() e a comparação de nulos, e os achados 1, 2 e 4 deixam de existir; o 3 é corrigido no texto. A mudança está em andamento e passa por nova revisão independente antes do merge.

bdcdo added 2 commits October 13, 2026 14:25
…cusada

Os extras polars e all passam a exigir polars>=1.0. Nesse piso, o
pl.from_pandas recusa com erro a coluna de texto com valor de outro tipo, e
o to_pandas() devolve os objetos de uma coluna Object. A coluna de um campo
do modelo vai para Object só quando a conversão levanta erro, e a retomada
lê a coluna Object pelo to_pandas() do polars.

A coluna Object continua montada por map_elements: o construtor de Series
troca o membro de Enum pelo valor mesmo com dtype=pl.Object. O caso que
chega lá com um membro de Enum é o Enum cujo valor é membro de outro Enum,
agora coberto por teste.

O aviso e a referência da API dizem que o Enum fora de lista ou dict sai
pelo valor também na coluna Object.
Sem o teste, mandar para Object toda coluna com nulo (float com NaN, inteiro
com NA, datetime com NaT, texto com None, lista com None, coluna toda nula,
dict com campo nulo) passava na suíte inteira.

Os comentários de from_pandas dizem que o pânico do polars não é capturado e
que só algumas versões do construtor de Series inspecionam os valores. O teste
das colunas de entrada deixa de tratar Decimal à parte, que Series.equals
compara desde o piso atual.
@bdcdo

bdcdo commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Terceira revisão independente (piso polars>=1.0): sem achado que bloqueie

O piso subiu em 5d5a656b: saíram do diff a releitura da coluna Object na entrada e a comparação de nulos, e a coluna vai para Object só quando a conversão levanta. Um subagente novo revisou esse commit em worktrees descartáveis.

O que confirmou. Matriz de 95 tipos de campo por 6 cenários (completa, entrada pl.Series, linha em erro, interrompida, retomada com pendente, saída completa passada de novo) em 16 versões do polars, de 1.0.0 com os pisos de pandas e pyarrow até 2.0.0: nenhum caso em que a main devolve e o branch devolve outra coisa ou levanta. Toda diferença é a main levantando ArrowInvalid, ArrowTypeError, TypeError, OverflowError ou ComputeError e o branch devolvendo Object, ou o Enum saindo pelo valor. As colunas de entrada voltam iguais nas 7.336 saídas, toda saída com coluna Object tem o aviso, e a retomada de coluna Object não falha em nenhuma das 16 versões. Jobs do CI reproduzidos, com polars 1.0.0, pandas 2.1.2 e pyarrow 10.0.0 no de versões mínimas.

Corrigido em 53d1986a:

  • O teste test_coluna_com_nulo_continua_tipada_e_sem_aviso tinha saído com a comparação de nulos, e sem ele cinco mutações que mandam coluna com nulo para Object passavam na suíte inteira. Voltou, e a mutação cai no polars 1.0.0 e no 2.0.0.
  • O comentário dizia "seja qual for o erro"; o pânico do polars não é Exception e não é capturado. O comentário sobre o construtor de Series passa a dizer que só algumas versões inspecionam os valores (medido: de 1.17.0 em diante).
  • O teste das colunas de entrada deixa de tratar Decimal à parte.

Não corrigido, com o motivo. Os quatro casos abaixo são iguais na main, e nenhum é regressão do PR:

  • Decimal acima de 38 dígitos levanta PanicException em todas as versões, depois das chamadas. Não há except que o pegue sem capturar BaseException.
  • No polars 1.6.0 a 1.9.0, um campo dict[str, int] com todas as respostas {} volta da conversão com 0 linhas. Pegar o caso pede uma conferência de tamanho que só essa faixa exercitaria, o mesmo tipo de contorno que a subida do piso tirou.
  • Union[time, int] com inteiro fora do intervalo de um dia sai Time com nulo no lugar do inteiro, do polars 1.8.2 em diante. A comparação de nulos removida pegava esse caso e só ele, em 14.792 colunas de tipos mistos; outras trocas caladas do pyarrow, como Union[date, int], ela nunca pegou. É a mesma família da Saída polars: campo que aceita date e datetime perde a hora quando a primeira resposta é só data #228.
  • polars 1.0.0 com pandas 3: uma coluna de texto com nulo, passada de novo com resume=True, sai Object com aviso. Na main o mesmo caso levanta TypeError.

Suíte em 53d1986a: 1184 testes em Python 3.13 e 1179 em 3.10 na configuração do job unit, 1178 nas versões mínimas, 1310 com todos os extras; ruff, ty, vulture e deptry limpos.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Entrada polars: a conversão final falha depois das chamadas quando o campo é Enum, tupla ou união mista

1 participant