Um Laço no Tempo


“Tempo e espaço são modos pelos quais pensamos e não condições nas quais vivemos.
A distinção entre passado, presente e futuro é só uma ilusão, ainda que muito persistente.”

Albert Einstein.

Existe no universo uma linha de eventos incomum, no sentido de extraordinária ou rara. Em algum lugar no tempo e do espaço alguém conseguiu emitir partículas que atingiam um detector microsegundos antes de sua emissão. Muitos não acreditaram, como ainda não acreditam, na possibilidade desses saltos temporais, e argumentaram que simplesmente “não se vê todo dia a chegada de viajantes do futuro”.

No entanto, e apesar do descrédito entre seus pares, a pessoa conseguiu aperfeiçoar seu experimento transferindo por intervalos de tempo mais separados objetos cada vez maiores. Incapaz de detectar esses objetos nas regiões intermediárias entre a emissão e a detecção ela criou a hipótese de que eles estavam viajando por regiões “interstícias” do espaço, fora ou além das quatro dimensões tradicionais. Pelo menos foi esse o nome e o conceito que ela usou.

Pouco tempo se passou, de acordo com sua própria contagem, até que ela conseguiu transferir microorganismos, depois pequenos animais. É claro que alguém com tamanha curiosidade e engenho, como poderíamos esperar, não demoraria muito até preparar todo um aparato para mandar a si mesma para o passado.

Por convenção e facilidade de relato a chamaremos de P. Pois P chegou ao mundo, na mesma linha de eventos, mas 40 anos antes da partida. Sem mencionar sua origem ela encontrou e fez amizade com os pais que, como logo percebeu, se conheciam mas se odiavam. Ela foi tomada pela ansiedade: talvez não devesse interferir com o fluxo dos eventos mas a cada dia, quando se aproximava a data provável de sua concepção e nenhum dos dois dava qualquer sinal de interesse pelo outro, mais crescia sua angústia.

P tentou se aproximar dos jovens enaltecendo, um para o outro, as figuras daqueles que teriam que gerá-la. Por infortúnio ela usou termos impróprios para aquele tempo, provocando maior antipatia e rejeição entre os dois. A situação se tornou irremediável. Não havia tempo para reconstruir um estado de coisas favorável pois, mesmo que os pais se relacionassem e tivessem um filho, este filho não seria P. Portanto, na narrativa dessa linha, P não nasceu. O mundo e sua história se bifurcaram. Desesperada ela tentou retomar sua vida na mesma linha de mundo de onde havia saído. Desapontada descobriu que não conseguiria, naquela época, equipamentos com tecnologia adequada. Sua tentativa foi um fracasso e P desapareceu em algum lugar e algum momento.

Portanto existe na história um laço de um único indivíduo que nasceu, cresceu e deixou de existir após voltar até os momentos anteriores à sua infância. O primeiro universo, onde deveriam existir pessoas que conheceram e sentiam saudades de P, existiu durante o laço, o intervalo entre seu nascimento e fim. Um novo universo, que ela apenas visitou, surgiu em consequência.

Não é improvável que o mesmo tipo de coisa tenha ocorrido novamente, algumas ou muitas vezes. Também não é difícil imaginar que visitantes do futuro destruam o seu passado, por um ou outro motivo. Talvez será esse mesmo o motivo para não recebermos com frequência “visitantes chegando do futuro”.

Há quem diga que, em outra linha de mundo, P nunca viajou para o passado. Ela teria, eles defendem, se enviado para o futuro. Nesse caso não haveria quem relatasse o ocorrido e, sendo isso verdade, esse conto pode ser seguramente ignorado.


História da Matemática

História da Matemática

Como construir um Astrolábio: Jakob Köbel, 1524. (wikipedia)

Os Ativistas


“Aqueles que consideramos os grandes homens e mulheres só são grandes porque nós estamos de joelhos. Precisamos nos levantar!”.

Elysée Loustallot, jornalista francês, 1761 – 1790.

Eu e mais dois jovens colegas ativistas estávamos descendo os últimos degraus do prédio onde participamos das Conferências da ONU sobre o Clima quando fomos abordados por uma desconhecida. Ela fez três afirmações: “Não represento perigo para vocês”, “precisamos salvar uma Terra” e “vocês são capazes de executar essa tarefa”. Depois perguntou: “concordam em me ajudar?” Nos entreolhamos surpresos e, sem tempo para raciocinar, concordamos.

Senti um formigamento pelo corpo e ligeira náusea. Depois me vi em outro local, difícil de ser descrito. Primeiro vi vários instrumentos de controle, algo como o cockpit de um avião. Depois, olhando para fora, confirmei que estava no ar, em local muito afastado da superfície. A desconhecida se apresentou como Ana e acrescentou: “não sou daqui e nem de agora. Olhem pela janela!”

A Terra abaixo de nós se acelerava até que não podíamos mais ver continentes, nuvens nem oceanos. As cores se misturaram até formar um azul bem claro, uniforme e sem textura. Entendi que estávamos em alta velocidade, sem compreender como isso seria possível: “porque não sinto nosso veículo em velocidade ou sendo acelerado?”, perguntei. Ana riu: “cancelamento de inércia. Um efeito diferente será distinguível em breve. Prestem atenção!”

A Terra agora era um bola lisa, com bordas muito bem definidas. “Olhem!” Primeira a borda da esfera perdeu a nitidez. Em seguida várias outras esferas menores despontaram ao seu lado. Passados alguns segundos as esferas bifurcadas se dividiram também, formando uma flor complexa. “Estamos vendo muitas Terras”, disse Ana, “bifurcações de eventos à partir de um evento qualquer em uma delas. Vocês conhecem o conceito de muitos mundos, da mecânica quântica?”. Discutimos rapidamente o assunto. Conhecíamos aquela ideia sem saber que ela poderia ter uma aplicação prática.

Ana percebeu nossa desorientação. “Cada evento tem muitas maneiras de progredir no tempo. Chamamos de trajetórias essas maneiras. Aquelas de maior probabilidade formam as Terras que vocês estão vendo. As Terras improvavéis se dissolvem no espaço-tempo. A aplicação prática de tal abstração é a seguinte: observando eventos destruidores que se propagam em muitas versões ou trajetórias podemos saber se um evento tem potencial danoso para seres sencientes ou não.”

“Por exemplo, observem aquela evolução…” , ela apontou para uma das esferas entre o centro e as bordas. À princípio eu não consegui resolver um esfera única em meio a tantas outras. Aos poucos, discutindo com os colegas, comecei a ver uma esfera escurecida, no meio de outras mais brilhantes. Ana nos informou: “A cor escura decorre da abundância de CO2. Essa Terra está em processo de extinção da vida abrigada. Vocês estão vendo a morte lenta de um planeta.”

Um dos meus colegas perguntou: “se são tantas Terras, por que deveríamos nos precupar com a morte de uma delas?”. “Acaso você sabe em qual delas você vive?”, perguntou a mulher. “Mas essa discussão é irrelevante. Olhem por mais algum tempo.” Foi isso o que fizemos até perceber que a cor escura do planeta moribundo se espalhava para esferas adjacentes. Ana voltou a falar, com voz tensa: “efeitos dos eventos se espalham. A humanidade, em todas as trajetórias de mundos, está tornando inviável a vida humana. Esse não é um evento singular e muitas estrelas com vida senciente são destruídas nesse processo, quando suas civilizações atingem eras tecnológicas. A imagem tipo flor que vocês estão vendo … nós a chamamos de Orbis. Nosso objetivo é salvar o maior número possível de Orbis.”

Ana fez um gesto nos impedindo de fazer perguntas. “Vocês se lembram dos limites de poluição acordados pelos chefes de nações na reunião da ONU?” Concordamos com a cabeça. “Tais metas serão cumpridas?” Sabíamos que não. “Vocêm devem agir!” Apontando para nós três ela continuou. “Vocês serão dirigentes políticos ou científicos em seus respectivos países. Comecem agora a estudar formas de produção da energia limpa.” Um dos colegas perguntou: “sabemos como gerar energia limpa. Mas também sabemos que as formas alternativas de geração de energia são caras. A comunidade econômica mundial não tolera perdas.”

Ana parecia estar se preparando para se despedir de nós e não interrompeu nenhum de seus movimentos por causa da pergunta. Ela só disse: “quando chegar a sua vêz de agir, o que será em breve, a economia já estará combalida o bastante. Não poderão se opor a políticos e cientistas que apresentarem projetos sólidos e factíveis. E vocês não estarão sozinhos … boa sorte!”

Senti vertigem novamente, menos assustado agora que sabia o que estava ocontecendo. Nos encontramos no alto da escadaria, onde estávamos mais ou menos três minutos antes de nossa partida. Nos abraçamos: “temos que falar sobre isso!” e fomos para um bar na esquina mais próxima, para confirmar nossas impressões e combinar nossas atitudes futuras.


Desafios Matemáticos


Um problema de lógica razoavelmente difícil!
Vejo alguém de olhos azuis!

O Raio do Círculo

Figura 1

Segue um exemplo de um tipo de questão recorrente em testes de admissão em empresas de tecnologia. Outras soluções, além da aqui apresentada, são encontradas em sites, como por exemplo no canal do Youtube Universo Narrado.

Dois arcos perpendiculares seccionam um círculo, como mostrado na figura 1. Qual é o raio do círculo?

A Distância mais Curta

Figura 3

Dados dois pontos A e B que estão do mesmo lado de uma reta r e não são pontos desta reta, qual é o caminho mais curto ligando A e B e que toca a reta r, (figura 3)?

Gauss e a soma dos 100 primeiros inteiros

Conta-se que Gauss teria encontrado a soma dos 100 primeiros inteiros em 30 segundos, na escola primária. Seu professor, aborrecido com a algazarra que faziam as crianças, teria mandado que todos calculassem esta soma e Gauss apresentou a resposta rapidamente. Esta é, na verdade, uma operação que pode ser feita de cabeça se você tiver a criatividade de Gauss …

Rolagem de discos

Figura 6

Esta questão apareceu no SAT americano (um teste usado para admissões nas universidades, aplicado no mundo todo) em 1982. Apenas 3 alunos entre os 300 mil que fizeram o teste acertaram. Até os examinadores que prepararam o problema erraram a solução e a questão teve que ser retirada da pontuação. No entanto é possível resolver essa questão com conhecimentos do nosso ensino médio.

O raio do disco A (vermelho, na figura 6) é de 1/3 do raio do disco B (cinza). O disco A desliza sem escorregar sobre o disco B até dar uma volta completa em torno do disco B e retornar para a sua posição original.

Quantas voltas o disco A terá dado em torno de si mesmo?

Leia também

Desafios Lógicos

Sobre as guerras do passado


“Deplorável é a guerra que, para atender ao interesse de alguns, inflige sofrimento sobre muitos que não se beneficiarão de seus resultados”.

Tullian Altoi, Anais de uma Guerra Passada

Um fio claro e reluzente cortou a atmosfera. Dois outros vieram em seguida, traçando uma espiral até o solo. O ar estava denso e frio e o céu azul escuro com partes vermelhas onde o sol ainda alcançava. Mas nenhuma condição adversa poderia ser usada como argumento para evitar o comparecimento das partes pois aquele era o último recanto do sistema não atingido pelo conflito.

Debilitados pela violência, exaustos nos corpos e mentes, as três espécies concordaram em formar uma grande Assembléia. Conselheiros deveriam debater e estabelecer os termos certos e justos para o fim das agressões. Tarefa difícil, eles reconheciam, se considerada a natureza distinta dos adversários, mas urgente após a constatação unânime de que não haveria vencedores.

Representantes dos Aleteos e Arqueos prepararam juntos um salão enorme e magnífico, com beleza que se esperava capaz de quebrantar o ânimo do oponente mais belicoso. Eram seres amantes da poesia e da beleza, forçados a se unir contra o inimigo comum. Eles entraram antes no salão, verificaram a correta disposição das coisas e ocuparam seus lugares. Os Alphas, nome por eles mesmos escolhido, entraram depois de forma ruidosa e exibindo contrariedade pela necessidade de dialogar.

Apenas três representantes se postaram na frente da multidão. Altoi, o Aleteo, pediu silêncio aos inquietos, lembrando que procuravam ali, na verdade, apenas chancelar o que já estava claro como única saída: a convivência harmoniosa. Ele fez então um relato listando as características de cada grupo, coisas de que cada um se orgulhava mas eram exatamente aquelas que os separavam e que precisariam ser superadas.

“Somos diferentes e precisamos lidar com a diferença”, ele disse, com entonação ordinária, sem tentar exibir suas conhecidas habilidades de orador. “Nós, Aleteos, evoluímos em ambiente tranquilo, não conhecemos predadores e jamais passamos por carência de insumos necessários para viver. Somos a espécie mais antiga pelo mesmo motivo, pois nossa evolução foi lenta e sem solavancos. Tratamos cada membro de nosso grupo como irmão e irmã, e fazemos isso desde que reconhecemos a origem comum de todos os indivíduos de nossa sociedade”.

“Arqueos, por outro lado, cresceram em ambiente árido em todos os estágios de sua evolução. Não são guerreiros porque seus inimigos não são sencientes. Obrigados a viver em terreno pouco estável, driblando vulcões e terremotos, eles também cresceram em fraternidade. A única beleza evidente em seu habitat é a arte que eles mesmos produzem … aliás uma bela forma de arte”!

Altoi tentava manter simples o seu discurso, sem perder na imponência que considerava necessária para manter algum controle sobre seus ouvintes. Mesmo assim percebeu a inquietação crescente que vinha do plenário. A um sinal discreto seu um grupo de jovens Aleteos entrou no salão servindo uma bebida quente, de cheiro adocicado, nas mesas previamente adornadas. A beleza dos Aleteos era óbvia mesmo para os de outra espécie. Eles sorriam, convidando os participantes a provarem de sua bebida. Altoi retomou sua fala:

“Também temos entre nós os Alphas, guerreiros extraordinários e fabulosos estrategistas que, para garantir sua subsistência tiveram que derrotar outras espécies inteligentes em seu mundo natal. Não ouvirão de nós nenhuma crítica, nem serão tratados com desapreço pelo extermínio desses inimigos, pois todos sabemos da impossibilidade de viverem todos no mesmo ambiente”.

“Mesmo assim peço aos Alphas que entendam e reconheçam que são o grupo mais belicoso nessa guerra. A Psicologia Interespécie mostra que são indivíduos menos empáticos, mais centrados na individualidade, o que está em flagrante dissonância com a cooperatividade das duas outras raças”.

“Temos mecanismos psíquicos e cognitivos diferentes. Embora as três espécies atualmente convivam com a vida aérea e não estão restritos à superfície, Aleteos e Arqueos evoluíram em ambiente com estímulo cognitivo em uma dimensão extra, inacessível aos Alphas. Em milhares de anos viajamos e percebemos o mundo como seres voadores, enquanto os Alphas estiveram presos ao chão, muitas vezes ao subsolo.” Em uma óbvia manobra diplomática ele ainda acrescentou: “embora saibamos que no presente os Alphas são excelentes pilotos…”

Parcas, o líder Alpha, sorriu procurando disfarçar a antipatia. Ele tomou um gole de sua taça, depois se levantou e falou em tom muito diferente, com a eloquência de quem impele seus pares ao combate.

“Protestando contra os termos injuriosos e insinuações de meu nobre colega conselheiro, reconheço a necessidade de capitular. Afirmo que desistimos de nosso propósito de incorporar todas as áreas produtivas do sistema”. Voltando-se para o próprio grupo: “Afinal, nossa própria simulação indica que a continuidade da guerra equivale à destruição mútua. Qual é a saída proposta?”

Temus, o líder Arqueo, falou dessa vez: “primeiro sugerimos que todas as espécies tenham seu direito inalienável à vida reconhecido. Por vida quero significar independência, conforto na busca de sustento e garantia de que não seremos explorados por qualquer outra espécie. Decidiremos livremente sobre nosso futuro, exceto em questões que envolvam outra raça”.

“Propomos que se vote pela harmonia, de bom grado ou não, defendida e guardada por uma Justiça dotada de sua própria Polícia, mantida e financiada em partes iguais pelos três planetas”.

Altoi Aleteo fez um gesto de desânimo. Ele pediu que se aproximassem seus conselheiros que o convenceram da necessidade da corporação policial. Os três líderes pediram que os delegados debatessem entre si, depois votassem.

Agitação e burburinho normal de um debate importante tomou conta do ambiente. Em poucos casos pessoas se destacaram de seus grupos para debater com membros de outra espécie. Quando se julgaram satisfeitos eles votaram. O resultado indiscutível, mostrando que a maioria absoluta escolheu buscar a paz, foi exibido em um placar colocado acima e em frente de todos. Seguiu-se uma comemoração intercalada por alguns poucos xingamentos, impropérios ditos em língua que a maioria desconhecia.

Passado o alvoroço Altoi Aleteo e Temus Arqueo subiram juntos ao púlpito pedindo silêncio. Eles se revezaram para dar uma notícia surpreendente:

“Não podemos aceitar esse resultado”.

Dessa vez a agitação e o ruído foram mais intensos. “O que está acontecendo?”, se perguntavam. Parcas Alpha exigiu explicação imediata, no que foi atendido por Altoi Aleteo:

“Vocês tomaram uma bebida feita de material eletrocondutor, o que nos permitiu manipular suas percepções e emotividade. Basicamente alteramos seu sentimento de empatia, usando aquelas antenas ali posicionadas”, disse ele apontando para estruturas côncavas presas ao teto. “Nossa intenção é a de mostrar que existe outra maneira de encarar o mundo que não remove a competição mas adiciona empatia. Esses dois fatores promovem a convivência e a troca com benefícios mútuos. Portanto convidamos a todos, Alphas em particular, a meditarem e visualizarem um mundo de cooperação, enquanto durar o efeito da nossa intervenção. O que podemos fazer juntos?”

Ainda sob efeito ativador do chá os Alphas aceitaram ficar ali parados, olhando com a atenção interna para uma visão de mundo que nunca haviam antes contemplado. “Guardem na memória tudo o que puderem perceber”, disse com a voz tranquila o lider, conhecedor de que sem essa manobra o armistício não seria duradouro.

Passado algum tempo o efeito começou a se dissipar. Os Alphas se sentiram tomados por sensação ambígua feita de raiva e vergonha por terem sido dominados psicologicamente, ainda que também de reverência pela visão inovadora a que tiveram acesso. Em suas imaginações eles viram os três grupos se expandindo pela galáxia, incorporando outras raças desenvolvidas em seu império e ajudando as menos avançadas a progredir. Alguns também viram guerras e conflito, todas de escopo local e sem a capacidade de destruir o grande fluxo civilizatório.


O que ocorreu depois disso ficou perdido no tempo e não foi incorporado na história. Pelo que observamos hoje as guerras foram interrompidas donde se conclui que O Congresso foi bem sucedido. Nenhuma das espécies foi extinta uma vez que temos circulando em nosso DNA porcentagens variáveis mas equitativas de herança de cada uma delas.

Inesperadamente nossa herança bélica foi útil em muitas situações ainda que a predominância de nossa natureza cooperativa tenha sido, de fato, responsável pela longevidade e alcance de nossa raça única e gloriosa!


Tabela de cores Html e CSS


As bibliotecas gráficas do Python, Matplotlib, Seaborn e Bokeh aceitam cores no sistema css e a maioria dos nomes ou shortcuts. O código hexadecimal é formado por 3 números, de 0 até 255 (ou 00 até FF), para daterminar a intensidade das cores R, G, B (vermelho, verde, azul). Dessa forma FFFFFF representa (255, 255, 255), ou branco; 000000 representa (0, 0, 0) ou preto. FF0000 é vermelho puro, 00FF00 é verde puro, 0000FF é azul puro. Além disso se pode utilizar o parâmetro alpha=a, onde 0 ≤ a ≤1 para a opacidade da cor, sendo 1 opacidade máxima.

Cores podem ser definidas como:

  • no formato hexadecimal (como #123456), com strings do CSS4 rgb(), rgba() ou hsl(): como rgb(0 127 0 / 1.0), rgba(255, 0, 127, 0.6) ou hsl(60deg 100% 50% / 1.0).
  • como tupla de inteiros (r, g, b), onde r, g, b são inteiros entre 0 e 255.
  • como a tuple (r, g, b, a), onde r, g, b são inteiros entre 0 e 255 e a é número de ponto flutuante entre 0 e 1.
  • um inteiro sem sinal representando RGBA values no padrão 0xRRGGBBAA, 0xffff00ff ou 0xff0000ff.

Segue uma tabela com as cores em código hexadecimal e seu nome.

Cor Hexadecimal Html/CSS Cor Hexadecimal Html/CSS Cor Hexadecimal Html/CSS
#000000 Black #000080 Navy #00008B DarkBlue
#0000CD MediumBlue #0000FF Blue #006400 DarkGreen
#008000 Green #008080 Teal #008B8B DarkCyan
#00BFFF DeepSkyBlue #00CED1 DarkTurquoise #00FA9A MediumSpringGreen
#00FF00 Lime #00FF7F SpringGreen #00FFFF Aqua
#00FFFF Cyan #191970 MidnightBlue #1E90FF DodgerBlue
#20B2AA LightSeaGreen #228B22 ForestGreen #2E8B57 SeaGreen
#2F4F4F DarkSlateGray #2F4F4F DarkSlateGrey #32CD32 LimeGreen
#3CB371 MediumSeaGreen #40E0D0 Turquoise #4169E1 RoyalBlue
#4682B4 SteelBlue #483D8B DarkSlateBlue #48D1CC MediumTurquoise
#4B0082 Indigo #556B2F DarkOliveGreen #5F9EA0 CadetBlue
#6495ED CornflowerBlue #663399 RebeccaPurple #66CDAA MediumAquaMarine
#696969 DimGray #696969 DimGrey #6A5ACD SlateBlue
#6B8E23 OliveDrab #708090 SlateGray #708090 SlateGrey
#778899 LightSlateGray #778899 LightSlateGrey #7B68EE MediumSlateBlue
#7CFC00 LawnGreen #7FFF00 Chartreuse #7FFFD4 Aquamarine
#800000 Maroon #800080 Purple #808000 Olive
#808080 Gray #808080 Grey #87CEEB SkyBlue
#87CEFA LightSkyBlue #8A2BE2 BlueViolet #8B0000 DarkRed
#8B008B DarkMagenta #8B4513 SaddleBrown #8FBC8F DarkSeaGreen
#90EE90 LightGreen #9370DB MediumPurple #9400D3 DarkViolet
#98FB98 PaleGreen #9932CC DarkOrchid #9ACD32 YellowGreen
#A0522D Sienna #A52A2A Brown #A9A9A9 DarkGray
#A9A9A9 DarkGrey #ADD8E6 LightBlue #ADFF2F GreenYellow
#AFEEEE PaleTurquoise #B0C4DE LightSteelBlue #B0E0E6 PowderBlue
#B22222 FireBrick #B8860B DarkGoldenRod #BA55D3 MediumOrchid
#BC8F8F RosyBrown #BDB76B DarkKhaki #C0C0C0 Silver
#C71585 MediumVioletRed #CD5C5C IndianRed #CD853F Peru
#D2691E Chocolate #D2B48C Tan #D3D3D3 LightGray
#D3D3D3 LightGrey #D8BFD8 Thistle #DA70D6 Orchid
#DAA520 GoldenRod #DB7093 PaleVioletRed #DC143C Crimson
#DCDCDC Gainsboro #DDA0DD Plum #DEB887 BurlyWood
#E0FFFF LightCyan #E6E6FA Lavender #E9967A DarkSalmon
#EE82EE Violet #EEE8AA PaleGoldenRod #F08080 LightCoral
#F0E68C Khaki #F0F8FF AliceBlue #F0FFF0 HoneyDew
#F0FFFF Azure #F4A460 SandyBrown #F5DEB3 Wheat
#F5F5DC Beige #F5F5F5 WhiteSmoke #F5FFFA MintCream
#F8F8FF GhostWhite #FA8072 Salmon #FAEBD7 AntiqueWhite
#FAF0E6 Linen #FAFAD2 LightGoldenRodYellow #FDF5E6 OldLace
#FF0000 Red #FF00FF Fuchsia #FF00FF Magenta
#FF1493 DeepPink #FF4500 OrangeRed #FF6347 Tomato
#FF69B4 HotPink #FF7F50 Coral #FF8C00 DarkOrange
#FFA07A LightSalmon #FFA500 Orange #FFB6C1 LightPink
#FFC0CB Pink #FFD700 Gold #FFDAB9 PeachPuff
#FFDEAD NavajoWhite #FFE4B5 Moccasin #FFE4C4 Bisque
#FFE4E1 MistyRose #FFEBCD BlanchedAlmond #FFEFD5 PapayaWhip
#FFF0F5 LavenderBlush #FFF5EE SeaShell #FFF8DC Cornsilk
#FFFACD LemonChiffon #FFFAF0 FloralWhite #FFFAFA Snow
#FFFF00 Yellow #FFFFE0 LightYellow #FFFFF0 Ivory
#FFFFFF White

Matplotlib


Quando lidamos com dados precisamos, muitas vezes, visualizar de forma gráfica esses dados. Em muitas tarefas é útil, ou até mesmo essencial, que as conclusões das análises sejam mostradas através de gráficos. Mesmo nas fases iniciais de uma análise, na preparação e limpeza de dados, a visualização é importante visualizar para se compreender padrões, tendências e anomalias, tais como pontos fora da curva. Existem no Python inúmeras bibliotecas para visualização de dados e montagem de gráficos. Matplotlib é o módulo básico para uso em conjunto com o pandas.

A biblioteca é grande, com extensas possibilidades e tem sido usada como base para a elaboração de outros módulos gráficos, como o Seaborn. O aprendizado da biblioteca inteira pode demandar um esforço considerável mas o uso básico, suficiente para muitos projetos, não demanda tanto empenho. Além disso o pandas tem uma vinculação natural com a biblioteca, como veremos.

Instalação

Matplotlib é instalado junto com a distribuição do python via Anaconda. Se você não está usando Anaconda é possível encontrar maiores instruções de instalação no site de Matplotlib.

Um pouco de história

Matplotlib começou a ser desenvolvida em 2003 por John D. Hunter, um neurocientista que usava MATLAB e queria aperfeiçoar a visualização de dados obtidos por meio de EEGs (eletroencelografia) em sua pesquisa sobre epilepsia. Hoje uma a comunidade de desenvolvedores colaboram para manter e aperfeiçoar a biblioteca.

Como muitos usuários e desenvolvedores estavam acostumados ao ambiente do MATLAB, onde todas as funções estão disponíveis globalmente sem a necessidade de importações, o módulo pylab foi desenvolvido. Ele existe para trazer funções e classes do NumPy e matplotlib para o namespace global. Isso significa que o comando from pylab import * em uma sessão significa a importação desses módulos e é desnecessária para quem está acostumado com o estilo do python. Como já vimos a importação de muitos módulos, funções e classes pode provocar conflito entre as partes importadas e os métodos built-in do python.

De fato, o uso de ipython --pylab para quem usa o comando de linha, ou %pylab de dentro do Jupyter, simplesmente faz uma chamada interna à from pylab import *. Nesse sentido se recomenda, para quem trabalha com IPython e Jupiter Notebook, que se use a “mágica” %matplotlib.

Por todos esses motivos usaremos a abordagem usual do python:

» import matplotlib.pyplot as plt
» import numpy as np
» np.random.seed(444)
» # para exibição dos gráficos no ambiente do jupyter notebook usamos
» %matplotlib
» # para exibição incorporada dentro do notebook
» %matplotlib inline
» # ou, para exibição dentro do notebook, com controles de zoom e arraste
» %matplotlib notebook

Numpy será usado para as contruções de arrays e geração de dados aleatórios. A informação de np.random.seed() serve para que os geradores produzam os mesmos números em seções posteriores, para reproducibilidade. A mágica %matplotlib faz com que os gráficos sejam exibidos. Nesse caso uma nova janela é aberta com uma barra de menus com acesso à ampliação, arraste, gravação em vários formatos (como pdf, jpg, png), e parâmetros do gráfico. Já a inserção de %matplotlib inline faz com que os gráficos fiquem embutidos no próprio notebook e sejam gravados com ele. A janela de controle não aparece. Usando %matplotlib notebook temos o gráfico embutido com acesso à controles de zoom, arraste e gravação em arquivo.

Técnica Básica

O matplotlib pode receber como fonte de dados listas, arrays do numpy, Series e dataframes do pandas. Por exemplo, o código seguinte recebe listas gera as figuras 1 e 2 abaixo:

» %matplotlib inline
» plt.plot([0,3,0,5,0,7,0])
» # a figura 1 é gerada

» plt.plot([0,1,2,3,4,5,6],[0,3,0,5,0,7,0])
» # o mesmo que antes (figura 1 é gerada)

» x = np.arange(101)-50
» y = x**2
» plt.plot(x,y)
» # a figura 2 é gerada

Quando apenas uma lista é fornecida plot usa os índices como coordenada horizontal (abcissa) e os valores da lista como coordenadas verticais (ordenadas). Quando duas listas de mesmo tamanho são fornecidas a primeira é usada para os valores das abcissas, a segunda como ordenadas. No segundo exemplo, que gera a figura 2, foram usadas coordenadas (x, x2) (uma parábola) com x variando no intervalo [-50, 50].

Usando %matplotlib notebook o gráfico é exibido inline mas trazendo controles de ajuste da imagem. Ao se clicar no botão azul (pode ter outra cor na sua instalação) os controles desaparecem e a imagem fica estática.

» %matplotlib notebook
» x = np.arange(40)
» plt.plot(x,np.exp(x/10))    # exibe a figura 3

» # outro exemplo: seno
» # dados a plotar 
» x = np.arange(0.0, 2.0, 0.01)
» y = np.sin(2 * np.pi * x)

» fig, ax = plt.subplots()
» ax.plot(x, y)

» ax.set(xlabel='eixo x', ylabel='y = seno(x)', title='Gráfico de seno(2 pi x)')
» ax.grid()

» fig.savefig('seno.jpg')
» plt.show()                  # exibe a figura 4

No primeiro caso plotamos simplesmente o gráfico de y = exp(x/10). No segundo exemplo criamos um array no intervalo [0, 2) em passos de .01. A coordenada é y = sen(2πx). Usamos as funções exponencial e seno do numpy para lidar com a operação vetorializada (que pode ser aplicada sobre todo o array). Já veremos com maiores detalhes os métodos de matplotlib.pyplot.

Hierarquia de objetos do Matplotlib

Mesmo em exemplos simples, como os anteriores, Matplotlib usa uma hierarquia de objetos. Por hierarquia se entende que objetos dependem de outros, como em ramos de uma árvore. Repetindo o gráfico da figura 4 temos:

†: O termo axes do matplotplib não se refere a “eixos” e sim a “figuras” individuais, dentro de uma Figure.

Figure é o objeto básico ou o mais externo de um gráfico. Ele pode conter diversos Axes, que são gráficos ou plotagens individuais. Axes, por sua vêz, podem conter legendas, marcas gráficas, curvas e caixas de texto. Cada um desses elementos são objetos do python com métodos e propriedades que podem ser manipuladas individualmente.

Vamos verificar em código como esses objetos são criados e manipulados.

» # geramos os dados a imprimir
» x = np.arange(0.0, 2.0, 0.01)
» y = np.sin(2 * np.pi * x)

» # O objeto básico do matplotlib.pyplot (aliás plt) é figure
» fig = plt.figure()
» # De figure derivamos um axes (subplot) e plotamos 3 curvas
» ax = fig.add_subplot()

» ax.plot(x, y)
» ax.plot(x, x)
» ax.plot(x, x**2)

» # definimos os labels dos eixos x e y e o título do gráfico
» ax.set(xlabel='eixo x', ylabel='seno(x), x, x^2',
»        title='y = seno, reta e parábola')
» # acrescentamos um quadriculado (grid)
» ax.grid()

» # salvamos a figura no disco
» fig.savefig('seno.jpg')
» # exibimos o resultado
» plt.show()

Ao objeto ax acrescentamos 3 plots (y=sen(2πx), y=x, y=x2), os labels dos eixos, o título do gráfico e o quadriculado de fundo. Opcionalmente a figura pode ser salva. A figura só é exibida quando plt.show() é executado.

O método add_subplot admite diversos parâmetros: fig.add_subplot(m,n,r) significa criar um gráfico em m linhas, n colunas, na posição r. Um subplot significa dividir a região destinada ao gráfico em m×n partes onde se pode colocar sub-gráficos.

Por ex., vamos criar uma figura com 4 subplots, plotando uma curva diferente em cada uma delas.

» x = np.arange(0.0, 2.0, 0.01)-1
» fig = plt.figure()

» ax1 = fig.add_subplot(2,2,1)               # 2 linhas, 2 colunas: a 1ª figura
» ax2 = fig.add_subplot(2,2,2)               # a 2ª figura
» ax3 = fig.add_subplot(2,2,3)
» ax4 = fig.add_subplot(2,2,4)

» ax1.plot(x, np.sin(10*x))
» ax2.plot(x, x)
» ax3.plot(x, x**2)
» ax4.plot(x, x**3)
» fig.savefig('figura5.jpg')

» plt.show()

A mesma figura pode ser obtida fazendo os plots diretamente para os axes:

» fig = plt.figure()
» ax1 = fig.add_subplot(2, 2, 1)
» plt.plot([-1, 0, 1, 2])
» ax2 = fig.add_subplot(2, 2, 2)
» ax3 = fig.add_subplot(2, 2, 3)
» plt.plot(np.random.randn(50).cumsum(), 'k--')
» ax4 = fig.add_subplot(2, 2, 4)
» plt.plot([1.5, 3.5, -2, 1.6])

O método plt.plot(dados) se refere ao eixo ativo que é aquele criado ou usado por último. No caso acima nenhuma figura foi plotada no 2 º retângulo.

O procedimento de criar vários subplots dentro de um mesmo gráfico pode resumido por meio do método
fig, axes = plt.subplots().
subplots() retorna uma tupla onde o 1&orm; elemento é uma Figure, o objeto básico de um plot, e o 2º são os axes que recebem as curvas.

Esses axes podem ser referenciados individualmente pela notação de array. Por ex.: em
fig, axes = plt.subplots(2, 3)
temos axes[0,0] até axes[1,2].

np.random.randn(50).cumsum()
retorna a soma cumulativa dos elementos de um array de 50 elementos “aleatórios”.

pyplot.subplots possui as opções:

nrows número de linhas
ncols número de colunas
sharex todos os subplots devem ter os mesmos “ticks” no eixo x
sharey todos os subplots devem ter os mesmos “ticks” no eixo y
subplot_kw dicionário de chaves para criar cada subplot
**fig_kw chaves adicionais, como plt.subplots(2,2,figsize=(8,6))

Formatação dos gráficos

Tamanho

O tamanho de uma figura, por default dada em polegadas, é definido pelo parâmetro figsize em plt.figure(figsize=(largura, altura)).

» x = np.arange(.1, 10, 0.01)
» largura = 5; altura = 2
» plt.figure(figsize=(largura, altura))
» plt.plot(x, np.log(x))
» plt.show()                       # gerado o gráfico na figura 8

O mesmo gráfico é gerado usando esse parâmetro no construtor da figure, mas desenhado com linhas pontilhadas, devido ao parâmetro ‘k–‘ em ax.plot(x, np.log(x), 'k--').

» x = np.arange(.1, 10, 0.01)
» fig = plt.figure(figsize=(5, 2))
» ax = fig.add_subplot()
» ax.plot(x, np.log(x), 'k--')
» plt.show()                       # gerado o gráfico na figura 9

Espaçamento entre subplots, cores e marcadores

O espaçamento entre figuras de um gráfico com subplots, que por default é um espaço não nulo, pode ser ajustado por meio do método Figure.subplots_adjust(). Por conveniência o mesmo método pode ser acessado diretamente pela função:
subplots_adjust((left=None, bottom=None, right=None, top=None, wspace=None, hspace=None).
wspace e hspace indica quanto espaço percentual em relação à largura e altura da figura, respectivamente.
Por exemplo, para juntar os subplots fazemos ambos igual a zero.

» x = np.arange(.1, 10, 0.01)
» fig, axes = plt.subplots(2, 2)
» axes[0, 0].hist(np.random.randn(1000), bins=100, alpha=.5)
» axes[0, 0].hist(np.random.randn(500), bins=50, color='r', alpha=.5)
» axes[0, 1].hist(np.random.randn(500), bins=50, color='gold', alpha=1)
» axes[1, 0].hist(np.random.randn(500), bins=50, color='#ff0000')
» axes[1, 1].plot(x, 3*np.log(x), color='#55aaff')
» axes[1, 1].plot(x, x, color='#000000')
» plt.subplots_adjust(wspace=0, hspace=0)


Para usar os mesmos eixos em todos os 4 gráficos usamos os parâmetros sharex, sharey, em
fig, axes = plt.subplots(2, 2), sharex=True, sharey=True).

Em hist(np.random.randn(1000), bins=100) traçamos o histograma de dados aletórios (100 números) separados em 100 bins. No 1º axes traçamos 2 histogramas, o 1º com a cor azul default, o 2º com color=’r’, um atalho para ‘red’ ou vermelho, com transparência de 50%, alpha=.5. Uma lista de cores nomeadas, como color=’gold’ pode ser encontrada no site do matplotlib. Também podemos usar o código de cores html que consiste em 3 números hexadecimais de 0 até 255 (ou 00 até ff em hexadecimal), no sistema rgb (vermelho, verde, azul). Diversos editores de imagens ou de código disponilizam um seletor de cores que retorna a cor nesse sistema. O site HTML-COLOR.CODES também tem um seletor online.


O método axes.plot(), além de aceitar arrays para abcissas e coordenadas, pode receber também o string especificador de cor e tipo de linha. Para imprimir em verde (‘green’) com linha tracejada usamos
ax.plot(x, y, 'g--'),
que é uma forma resumida de passar parâmetros. Isso é o mesmo que:
ax.plot(x, y, linestyle='--', color='g').
Considerando que x e y são arrays de mesmo tamanho, alguns exemplos desses parâmetros são:

» plot(x, y)            # plot x, y com linha e estilo default
» plot(x, y, 'bo')      # plot x, y com marcadores azuis circulares
» plot(x, y, 'rv')      # plot x, y com marcadores vermelhos, triângulo para baixo
» plot(y)               # plot y usando seus índices como coordenadas-x
» plot(y, 'r+')         # idem, usando cruzes vermelhas

No Jupyter Notebook use plt.plot? para ver uma lista completa dos parâmetros desse método.

Ao desenhar um gráfico pode ser interessante marcar os pontos sobre as curvas contínuas. Isso é feito com markers ou marcadores.

» from numpy.random import randn
» plt.plot(randn(30).cumsum(), color='green', linestyle='dashed', marker='o')

Uma forma abreviada para o mesmo comando é: plt.plot(randn(30).cumsum(), 'go--'), onde os parâmetros são passados em uma única string, com g para green (verde), o para o marcador circular e -- para o estilo de linha tracejado.

Por default os pontos de um plot são ligados por linhas. Para outro estilo usamos drawstyle:

» data = np.random.randn(20).cumsum()
» plt.plot(data, 'b--', label='default', marker='v')                      # linha azul
» plt.plot(data, 'r-', drawstyle='steps-post', label='passos')            # linha vermelha
» plt.legend(loc='best')


A linha azul é tracejada (‘b–‘), no estilo default e com marcadores ‘v’ (triângulos). O parâmetro label cria legendas, nesse caso indicando o texto ‘defaul’. A linha vermelha tem estilo drawstyle=’steps-post’ (em passos) e é marcada na legenda como ‘passos’. plt.legend(loc='best') informa que o melhor local para colocar essa legenda seja encontrado automaticamente. Outros valores seriam: loc='right', 'center', 'upper right', etc.

Marcas, etiquetas e legendas (ticks, labels, legends)

» dados = np.random.randn(1000)
» cumulativo = dados.cumsum()

» fig = plt.figure()
» ax = fig.add_subplot(1,1,1)
» ax.plot(10*dados + 10)
» ax.plot(cumulativo)


As duas plotagens são sobrepostas no único axes criado. O operação 10*dados+10 serve apenas para efeito estético da apresentação dos dados aleatórios.

Vamos usar os mesmos dados para verificar as propriedades de ajuste do título global do gráfico, labels nos eixos x e y, label do gráfico e ticks para melhorar a apresentação do gráfico anterior.

» fig = plt.figure()
» ax = fig.add_subplot(1,1,1)
» ax.set_title('Alterando eixos com matplotlib')
» ticks = ax.set_xticks([0, 250, 500, 750, 1000])
» ticks = ax.set_yticks([-20, 0, 20, 40, 60, 80])

» labels = ax.set_xticklabels(['seg' ,'ter', 'qua', 'qui', 'sex'],
»                             rotation=45, fontsize='large')
» labels = ax.set_yticklabels(['-A' ,'O', 'A', 'B', 'C','D'],
»                             fontsize='large')

» ax.set_xlabel('Ao longo dos dias...')
» ax.set_ylabel('Observado')

» ax.plot(10*dados, color='#55aaff', alpha=.5, label='dados')
» ax.plot(cumulativo, color='red', label='cumulativo')
» ax.legend(loc='best')

Anotações e desenhos nos subplots.

Diversos tipos de anotações, setas e desenhos podem ser incluídos nos gráficos. Para traçar os gráficos seguintes vamos usar o arquivo .csv do Our World in Data, baixados para a pasta ./dados.

Primeiro importamos o arquivo baixado .csv para um dataframe. Esse arquivo contém dados dos países do mundo, nos anos de 1950 até 2099, contendo número de nascimentos verificados até 2020 e valores interpolados para os anos seguintes. O dataframe original tem o seguinte formato:

Em seguida selecionamos apenas os dados sobre o Brasil.

» dados = pd.read_csv('./dados/number-of-births-per-year.csv')
» dados=dados[(dados['Entity']=='Brazil')]
» dados.head(2)

» # as colunas 3 e 4 têm nomes longos, que vamos renomear
» dados.columns[3],dados.columns[4]
↳ ('Estimates, 1950 - 2020: Annually interpolated demographic indicators - Births (thousands)',
↳  'Medium fertility variant, 2020 - 2099: Annually interpolated demographic indicators - Births (thousands)')

» dados.rename(columns={'Year':'ano',
»                       dados.columns[3]:'nasc',
»                       dados.columns[4]:'inter'}, inplace=True)
» # copiamos os dados da coluna de interpolação, após 2020, para a colunas de nascimentos
» dados.loc[dados['nasc'].isna(), 'nasc'] = dados['inter']

» # vamos mantes apenas as colunas 'ano' e 'nasc'
» dados = dados[['ano', 'nasc']]
» dados.head()
↳         ano          nasc
↳ 4050    1950    2439820.0
↳ 4051    1951    2467186.0
↳ 4052    1952    2523577.0
↳ 4053    1953    2583285.0
↳ 4054    1954    2646311.0

Para usar como anotações no gráfico encontramos os anos em que  nascimentos foram máximo e mínimo, além do ano em que se inicia a interpolação, 2020.

» maior=dados[dados['nasc']==dados['nasc'].max()]
» menor=dados[dados['nasc']==dados['nasc'].min()]

» ano_maior = maior['ano'].values[0]
» nasc_maior = int(maior['nasc'].values[0])

» ano_menor = menor['ano'].values[0]
» nasc_menor = int(menor['nasc'].values[0])

» interX = 2020   # início da interpolação
» interY = int(dados[dados['ano']==2020]['nasc'].values[0])

» txt = ('Máximo de nascimentos:\t {} no ano {}.\n'
»         'Mínimo de nascimentos:\t {} no ano {}.\n'
»         'Início da interpolação:\t {} no ano {}.'
»       )

» print(txt.format(nasc_maior, ano_maior,nasc_menor, ano_menor, interY, interX))
↳ Máximo de nascimentos:	 3929646 no ano 1983.
↳ Mínimo de nascimentos:	 1504597 no ano 2099.
↳ Início da interpolação:	 2859135 no ano 2020.

Com esses dados imprimimos o gráfico (sem muita preocupação estética). Uma primeira curva é traçada em preto, incluindo os anos de 1950 até 2020. A segunda curve se inicia em 2021 até o final e é tracejada em vermelho, para indicar a interpolação. Uma terceira curva tem efeito decorativo, em azul e transparente.

» fig = plt.figure()
» ax = fig.add_subplot()
» ax.set_title('Nascimentos (em milhões), por ano', size=18)
» ax.plot(dados[dados['ano']<2021]['ano'], dados[dados['ano']<2021]['nasc'], color='black', alpha=1)
» ax.plot(dados[dados['ano']>2020]['ano'], dados[dados['ano']>2020]['nasc'], 'r--')
» ax.plot(dados['ano'], dados['nasc'], 'b', linewidth=5, alpha=.2)
» ax.grid(color='grey', alpha=.3 )
» ax.annotate('Máximo', xy=(ano_maior, nasc_maior), size=13)
» ax.annotate('Mínimo', xy=(ano_menor, nasc_menor), size=13)
» ax.annotate('Interpolado', xy=(interX, interY), size=13)
» ax.arrow(ano_maior-10, nasc_maior, 10, 0)
» ax.arrow(interX-10, interY-10, 10, 10)            # a figura 17 abaixo é plotada


Para dar um zoom na figura podemos limitar as faixas de valores no eixo x e eixo y com ax.set_xlim(a,b) e ax.set_xlim(m,n), respectivamente, em torno do ponto de interesse. No exemplo fazemos um zoom em torno do ponto de máximo, obtendo o gráfico 18 acima.

» ax.set_xlim([1980, 1990])
» ax.set_ylim([3.76e6, 3.95e6])
» fig.get_figure()                                  # a figura 18 acima é plotada

Figuras sobre o plot

Diversas formas mais comuns estão disponíveis para inserção nos plots, e são chamadas de patches no matplotlib. Algumas delas estão diretamente em matplotlib.pyplot como retângulos, círculos e polígonos. Muitas outras estão em matplotlib.patches. Para traçar figuras construimos os patches com os métodos apropriados e os acrescentamos ao subplot usando ax.add_patch().

retângulo: plt.Rectangle((x, y), largura, altura), onde (x, y) são as coordenadas do ponto inferior esquerdo,
círculo: plt.Circle((x_0, y_0), raio), onde (x_0, y_0) são as coordenadas do centro,
polígono: plt.Polygon([[x_0, y_0], [x_1>, y_1],…, [x_n, y_n]).

No caso do polígono a área interna às retas que ligam os pontos é colorida.

» fig = plt.figure()
» ax = fig.add_subplot()
» retangulo = plt.Rectangle((0.2, 0.2), 0.6, 0.4, color='#aabbcc')
» circulo = plt.Circle((0.4, 0.6), 0.3, color='plum', alpha=0.3)
» poligono1 = plt.Polygon([[0.1, 0.1], [0.8, 0.7], [.3,.7], [0.6, 0.1]], color='turquoise', alpha=0.8)
» poligono2 = plt.Polygon([[0.2, 0.2], [0.8, 0.8]], color='red', alpha=0.8)
» ax.add_patch(retangulo)
» ax.add_patch(circulo)
» ax.add_patch(poligono1)
» ax.add_patch(poligono2)

Para o segundo “polígono” apenas dois pontos foram fornecidos e ele é representado pela reta (vermelha) que liga esses pontos.

Claro que gráficos mais elaborados podem ser montados com uma combinação de figuras como retas, polígonos, círculos, etc. No caso abaixo uma cor é escolhida “aleatoriamente” para plotar um série de 2 triângulos, um com um vértice em (0,1), outro em (1,0). Os dois outros vértices dos triângulos são coincidentes, e se deslocam sobre a reta (t,t) com t de 0 a 1, com espaçamento .1. A cor tem transparência alpha=.6 para que as cores apareçam em tons pastéis.

A função cor() retorna uma tupla (r,b,g) onde cada componente representa as cores vermelho, verde e azul, com valores de 0 até 1.

» def cor():
»     return (np.random.random(), np.random.random(), np.random.random())

» fig = plt.figure()
» ax = fig.add_subplot()
» for t in np.linspace(0,1,100):
»     c = cor()
»     poligono1 = plt.Polygon([[0, 1], [t, t], [t+.1, t+.1]], color=c, alpha=.6)
»     poligono2 = plt.Polygon([[1, 0], [t, t], [t+.1, t+.1]], color=c, alpha=.6)
»     ax.add_patch(poligono1)
»     ax.add_patch(poligono2)                       # a figura 20 é gerada


O código abaixo gera círculos de raios aleatórios, espalhados em torno da reta (t,t), afastados dela por uma variacão também aleatória.

» fig = plt.figure()
» ax = fig.add_subplot()
» plt.axis('equal')
» for t in np.linspace(0,1,100):
»     circulo = plt.Circle((t*np.random.random(), t*np.random.random()), np.random.random()/10, color=cor(), alpha=0.5)
»     ax.add_patch(circulo)
» ax.set_xlim([0, 1])
» ax.set_ylim([0, 1])
» plt.savefig('circulos.pdf')                       # a figura 21 é gerada

Ao final a figura gerada é gravada em disco com o formato “pdf”. Outros formatos podem ser escolhidos, como “jpeg”, “png”, “svg”, assim como a resolução em dots per inches, (dpi ), que tem default = 100, além da cor de fundo e bordas.

Configuração do matplotlib

Por padrão o matplotlib possui um esquema de cores e outros parâmetros, como largura e tipo de linhas, previamente definidos e voltados para plotar figuras prontas para publicação. No entanto, vários destes parâmetros podem ser personalizados através de ajustes nos valores globais tais como tamanho, espaçamento entre subplots, cores, família e tamanhos de fonte, estilos de grade, etc.

Uma forma de alterar esses padrões está no método plt.rc (parametro, opcoes) onde parametro é uma string com o nome do parâmetro que se quer modificar, e opcoes é uma sequência de argumentos de palavras-chaves com os novos valores.

Entre outras opções parametro pode ser figure, axis, xtick, ytick, grid, legend. As opções podem ser passadas de várias formas. O exemplo mostra como ajustar todas as figuras de uma sessão para o tamanho 20×15. Depois fazemos ajustes às fontes, usando um dicionário.

» # ajustar tamanho da figura
» plt.rc ('figure', figsize = (20, 15))

» # parâmetros associados às fontes, em um dicionário
» font_options = {'family' : 'monospace', 'weight' : 'bold', 'size' : 'small'}
» plt.rc ('font', **font_options)

Uma personalização mais ampla pode ser feita no arquivo de configurações. Para encontrar esse arquivo use os comandos:

» # no prompt do sistema digite
» python -c "import matplotlib; print(matplotlib.matplotlib_fname())"

» # de dentro do jupyter notebook (ou de qualquer ambiente em que você trabalhe):
» import matplotlib
» print(matplotlib.matplotlib_fname())
↳ /home/usuario/.anaconda3/lib/python3.8/site-packages/matplotlib/mpl-data/matplotlibrc

Esse output é relativo ao sistema e à distribuição que está sendo usada, lembrando que é possível existir mais de uma instalação em um computador. No caso mostrado está em uso o anaconda e jupyter no linux mint. Cada usuário pode encontrar um local diferente. Esse arquivo não deve ser editado diretamente mas copiado para a pasta home do usuário (no linux) com o nome .matplotlibrc. Desta forma ele será carregado durante a inicialização do pacote. A análise desse arquivo é uma boa forma de se conhecer as possibilidades na personalização, sendo que as opções estão comentadas.

As atuais configurações globais podem ser vistas com o comando

» import matplotlib as mpl
» print(mpl.rcParams)

matplotlib.rcParams é usado para alterar esses parâmetros, um de cada vez. matplotlib.rc pode alterar os valores default para vários parâmetros de um grupo específico, como tipos de lihnes, fontes, textos, etc.

» matplotlib.rcParams['lines.markersize'] = 20
» matplotlib.rcParams['font.size'] = '15.0'

matplotlib.rcdefaults() reseta todos os parâmetros para seus valores originais.

Usando matplotlib com o pandas

A própria biblioteca do pandas embute diversas funcionalidades do matplotlib, sem que esse tenha que ser carregado explicitamente. Isso significa que podemos criar gráficos sem passar por todas as etapas de sua construção.

Por exemplo, uma instância de Series possui o método series.plot().

» s1 = pd.Series(np.random.randn(100).cumsum())
» s1.plot(use_index=False)                          # grafico 22-a é plotado

» s2 = pd.Series([x**2 for x in np.arange(-10,10,.1)], index=np.arange(-10,10,.1))
» s2.plot()                                         # grafico 22-b é plotado


Na figura 21 o eixo x recebe valores dos índices da série, que por default vai de 0 até 99. Na segunda o índice que foi declarado é usado. Para evitar o procedimento de usar o índice como ordenada passamos o parâmetro series.plot(use_index=False).

Para um dataframe cada series correspondente a cada coluna é plotada separadamente. Abaixo construimos e plotamos um dataframe de quatro colunas, cada uma delas representando valores de um seno com frequências diferentes.

» s1 = pd.Series([np.sin(x) for x in np.arange(0,10,.1)])
» s2 = pd.Series([np.sin(2*x) for x in np.arange(0,10,.1)])
» s3 = pd.Series([np.sin(3*x) for x in np.arange(0,10,.1)])
» s4 = pd.Series([np.sin(4*x) for x in np.arange(0,10,.1)])

» df=pd.concat({'A': s1, 'B': s2, 'C': s3, 'D': s4} , axis=1)

» df.plot()                                                        # a figura 23 é plotada
» df.plot(color=['k','r','b','y'], alpha=.6, logx=True, grid=True) # a figura 24 é plotada


O dataframe df contém 4 colunas, cada uma com os valores de seno(πx), seno(2πx), seno(3πx), seno(4πx), com x variando de 0 a 10 em passos de 0,1. No segundo gráfico, figura 24, alguns parâmetros foram passados, como uma lista de cores, a existência de quadriculado (grid) e a instrução para usar uma escala logarítmica em x.

A instrução dataframe.plot() é um atalho para dataframe.plot.line() que representa como curvas os pontos passados. Outros parâmetros podem ser passados, exatamente como no uso direto de matplotlib:

Argumento Descrição
label texto para a legenda
ax objeto subplot do matplotlib onde plotar. Se vazio os plots vão para o subplot ativo
style string de estilo, como ‘ko–‘, passado para o matplotlib
alpha opacidade do plot (de 0 to 1)
kind opções: ‘area’, ‘bar’, ‘barh’, ‘density’, ‘hist’, ‘kde’, ‘line’, ‘pie’
logy use escala logaritmica no eixo y
use_index use o index para os labels de x
rot rotação de texto nos labels (0 até 360)
xticks valores a usar para marcas no eixo x
yticks valores a usar para marcas no eixo y
xlim limites para o eixo x (ex.: [0, 10])
ylim limites para o eixo y
grid exibir grade quadriculada de fundo (grid), default=exibir

Alguns parâmetros são específicos para dataframes.

Argumento Descrição
subplots bool. Plota cada coluna em um subplot separado
sharex se subplots=True, use o mesmo eixo x, com marcas e limites
sharey se subplots=True, use o mesmo eixo y, com marcas e limites
figsize tupla indicando tamanho da figura
title texto para o título
legend bool. Inclui legenda do subplot (default=True)
sort_columns plot colunas em ordem alfabética no nome; default= ordem no dataframe

Plotagem em barras

A plotagem em barras (bar plots ) pode ser feita com plot.bar() e plot.barh() (com barras verticais e horizontais).

Para experimentar com esses tipos de plotagens vamos usar os mesmos dados importados anteriormente, que contém uma lista de países com o número de nascimentos por ano de 1950 até 2020, e a estimativa à partir de 2021. Importamos o arquivo .csv para um dataframe e selecionamos apenas as linhas relativas ao ano de 2020. Linhas relativas à continentes e outras partes do mundo que não países possuem coluna Code = NaN e são excluídas. Renomeamos as colunas para mais fácil manuseio e mantemos apenas as colunas relativas ao país, ano e número de nascimentos.

» # importação do csv em um dataframe
» dados = pd.read_csv('./dados/number-of-births-per-year.csv')
» # selecão do ano = 2020 e apenas países
» dados = dados[(dados['Code'].notnull()) & (dados['Year']==2020)]
» # renomeando colunas
» dados = dados.rename(columns={'Entity':'país','Year':'ano', dados.columns[3]:'nasc'})
» # mantendo apenas colunas relevantes
» dados = dados[['país', 'nasc']]

» # use a coluna 'país' como índice
» dados.set_index('país', inplace=True)

» # o dataframe final:
» dados.head(4)
↳                      nasc
↳        país
↳ Afghanistan     1215628.0
↳     Albania       32888.0
↳     Algeria      995368.0
↳      Angola     1311356.0

Imprimimos os 2 tipos de barplot com o código abaixo, usando 10 países para as barras verticais e 20 para as horizontais. Para isso inicializamos uma figura com 1 linha e 2 colunas. O parâmetro figsize=(15, 16) indica que nossa figura terá a largura 15 e altura 6 (em polegadas). Dados os nomes longos de países o gráfico ficou sobreposto, o que seria controlado aumentando-se a separação entre axes.

» fig, axes = plt.subplots(2, 1, figsize=(10, 10))
» dados[:10].plot.bar(ax=axes[0], color=['r','b','g'], rot=30, grid=True)
» dados[:20].plot.barh(ax=axes[1], color='g', alpha=0.7, grid=True)


Gráficos desse tipo são desenhados para series e dataframes com apenas uma coluna. Se o dataframe possui várias colunas o gráfico de barras plota uma barra para cada coluna.

Para o próximo exemplo usaremos os dados disponibilizados no GapMinder, já usados e descritos nesse site. Desses dados manteremos apenas as colunas que renomearemos como “pais”, “ano” e “pop” (população), ficando com um dataframe com os países de mundo e suas populações nos anos listados abaixo.

Dessa coleta de dado separamos apenas os 5 países com maior população no último ano, 2007.

» # baixar dados do gapminder
» url =(
»       'https://raw.githubusercontent.com/jennybc/'
»       'gapminder/master/data-raw/08_gap-every-five-years.tsv'
»        )

» # criamos o dataframe dfPaises. O arquivo importado tem campos separados por tabs
» dfPaises = pd.read_csv(url, sep='\t')

» # para ver colunas e forma geral usamos
» dfPaises.head(2)
↳         country   continent    year    lifeExp        pop     gdpPercap
↳ 0   Afghanistan        Asia    1952     28.801    8425333    779.445314
↳ 1   Afghanistan        Asia    1957     30.332    9240934    820.853030

» # usamos apenas 3 colunas
» dfPaises = dfPaises[['country','year','pop']]
» # e as renomeamos
» dfPaises.rename(columns={'country':'pais', 'year':'ano'}, inplace=True)

» # o resultado é
» dfPaises
↳             pais     ano        pop
↳ 0    Afghanistan    1952    8425333
↳ 1    Afghanistan    1957    9240934
↳ 2    Afghanistan    1962   10267083

» # para ver os anos registrados examinamos o conjunto (set)
» set(dfPaises['ano'])
↳ {1952, 1957, 1962, 1967, 1972, 1977, 1982, 1987, 1992, 1997, 2002, 2007}

» # países mais populosos, em 2007
» dfPaises[dfPaises['ano']==2007] \
»          .sort_values(by=['pop'], axis=0, \
»           ascending=False, inplace=False)['pais'] \
»          .head(5)
↳ 299             China
↳ 707             India
↳ 1619    United States
↳ 719         Indonesia
↳ 179            Brazil

O último comando, para selecionar os países mais populosos, está quebrada pelo caracter \ (back slash) que é ignorado (a linha é executada por inteiro). Essa linha pode ser compreendida assim:

dfPaises[dfPaises['ano']==2007]                         : seleção só de linhas com ano = 2007
.sort_values(by=['pop'], axis=0, ascending=False)       : ordena pela coluna 'pop' em ordem inversa
['pais'].head(5)                                        : só a coluna 'pais', 5 primeiros valores

Em seguida montamos um dataframe para cada desses países e os concatenamos para um dataframe mais geral que contém linhas indexadas pela ano e colunas com o nome do país. (Outras técnicas de agrupamento serão vistas mais tarde.)

» china = dfPaises[dfPaises['pais']=='China'][['ano', 'pop']]\
»         .set_index('ano').rename(columns={'pop':'china'})
» india = dfPaises[dfPaises['pais']=='India'][['ano', 'pop']]\
»         .set_index('ano').rename(columns={'pop':'india'})
» usa = dfPaises[dfPaises['pais']=='United States'][['ano', 'pop']]\
»       .set_index('ano').rename(columns={'pop':'usa'})
» indonesia = dfPaises[dfPaises['pais']=='Indonesia'][['ano', 'pop']]\
»             .set_index('ano').rename(columns={'pop':'indonesia'})
» brasil = dfPaises[dfPaises['pais']=='Brazil'][['ano', 'pop']]\
»          .set_index('ano').rename(columns={'pop':'brasil'})

As linhas de seleção de dados do país podem ser compreendidas assim:

  dfPaises[dfPaises['pais']=='Brazil']      : seleciona apenas linhas relativas ao país 'Brazil'
  [['ano', 'pop']]                          : desse df copia apenas as colunas 'ano' e 'pop'
  .set_index('ano')                         : use a coluna 'ano' como índice
  .rename(columns={'pop':'brasil'})         : renomeie coluna 'pop' para 'brasil'
» # os paises são concatenados em um único df
» df = pd.concat([china, india, usa, indonesia, brasil],  axis=1)
» # o nome da lista de colunas será usado no plot
» df.columns.name = 'População'

» # e o resultado é
» df.head(3)
↳ População     china        india          usa   indonesia      brasil
↳ ano
↳ 1952      556263527    372000000    157553000    82052000    56602560
↳ 1957      637408000    409000000    171984000    90124000    65551171
↳ 1962      665770000    454000000    186538000    99028000    76039390

» # esse dataframe pode ser exibido em gráfico de barras
» cor = ['salmon','gold','teal','plum','powderblue']
» df.plot.bar(figsize=(10,5), grid=True, color = cor, title='Países mais populosos (de 1952 até 2007)', rot=45)

Na plotagem acima uma paleta de cores foi passada para o parâmetro color. Cada uma delas é usada para um país. O nome das colunas, df.columns.name = 'População' é usado como título da legenda.

Para gerar gráficos de barras empilhadas (stacked bar ) passamos o valor stacked=True. Nos exemplos plotamos a versão horizontal e vertical do mesmo gráfico acima.

» df.plot.barh(stacked=True, alpha=.7)         # a figura 27 é plotada

» df.plot.bar(stacked=True, alpha=.7)          # a figura 28 é plotada

Histogramas

Um histograma é uma representação gráfica, similar a um gráfico de barras, de uma distribuição de pontos. Os pontos são distribuídos em faixas igualmente divididas e o gráfico é o conjunto de retângulos com base de tamanho igual à largura das faixas e altura correspondente ao número de pontos em cada faixa.

Criamos uma série com 1000 números aleatórios, multiplicados por 100. O resultado é, no caso dessa execução, um conjunto distribuído entre -315 e 325 (aproximadamente). A partir desses dados traçamos o histograma e o gráfico de densidade ou density plot.

» ser = pd.Series((np.random.randn(1000)*100))
» ser.describe()
↳ count    1000.000000
↳ mean        3.747373
↳ std       102.637489
↳ min      -314.443835
↳ 25%       -65.343082
↳ 50%         4.835384
↳ 75%        73.314604
↳ max       324.011000

» ser.plot.hist(bins=10, grid=True, color='b', alpha=.4) # figura 19

» ser.plot.kde() # o mesmo que ser.plot.density()        # figura 30


O gráfico de densidade consiste na plotagem de uma função de distribuição de probabilidade que poderia ter gerado os dados na series. A técnica usual consiste em usar uma mistura de “núcleos” ou “kernels”. Esses gráficos são também chamados de estimativa de núcleos de densidade (kernel density estimate, KDE ).

Seaborn

Seaborn é outra biblioteca do Python voltada para a visualização de dados, baseada no matplotlib. Ela apresenta uma interface de mais alto nível e aprimoramento da qualidade estética dos gráficos. Com o Seaborn se pode conseguir gráficos bem elaborados e de boa aparência com um número menor de linhas de código.

Para os exemplos com o seaborn vamos usar os dados do Gapminder já descritos. O dataframe importado tem 1704 linhas com dados sobre os países, 6 colunas ‘country’, ‘continent’, ‘year’, ‘lifeExp’, ‘pop’, ‘gdpPercap’, respectivamente ‘pais’, ‘continente’, ‘ano’, ‘Expectativa de vida’, ‘população’, ‘PIB percapita’.

» url =(
»       'https://raw.githubusercontent.com/jennybc/'
»       'gapminder/master/data-raw/08_gap-every-five-years.tsv'
»        )
» # criamos o dataframe dfPaises. O arquivo importado tem campos separados por tabs
» dfPaises = pd.read_csv(url, sep='\t')

» # para restringir o volume dos dados armazenamos as fatias
» df2007 = dfPaises[dfPaises['year']==2007]
» dfBrasil = dfPaises[dfPaises['country']=='Brazil']

O seaborn deve ser importado. Para uma gráfico de barras mais simples informamos e base de dados e os nomes de colunas a usadas como valores para os eixos. Para não congestionar o gráfico usamos apenas os 10 primeiros países.

» import seaborn as sns
» sns.barplot(data=df2007[:10], x='lifeExp', y='gdpPercap')
» # é plotada a figura 31 abaixo

Muitas configuarações podem ser aplicadas sobre esse gráfico básico. Algumas são usadas abaixo, como a orientação das barras, textos e rotação nos eixos x e y.

» sns.set_style('darkgrid')
» graf = sns.barplot(data=df2007[:10], x='lifeExp', y='gdpPercap', orient='h')
» graf.set(xlabel = 'Expectativa de vida (anos)',
                     ylabel = 'PIB percapita', title ='Expectativa de vida x PIB')
» graf.set_xticklabels(labels=df2007[:10]['lifeExp'].round(1))
» graf.set_yticklabels(labels=df2007[:10]['country'], rotation=30)
» # é plotada a figura 32 acima

Valores válidos para sns.set_style() são 'white', 'dark', 'whitegrid', 'darkgrid', 'ticks'.

Esses gráficos plotados não sugerem qualquer relação entre a renda percapita e a expectativa de vida, o que é natural uma vez que escolhemos apenas os primeiros 10 países, em ordem alfabética. Claro que barplots não são apropriados para exibir um número muito grande de dados. Para isso podemos usar seaborn.regplot que plota o gráficos de dispersão (scatter plots ) e uma reta correspondente a um ajuste do modelo de regressão linear. Esse último gráfico mostra que existe correlação entre expectativa de vida e renda percapita.

» sns.regplot(x='lifeExp', y='gdpPercap', data=df2007) # figura 33


É comum em análise de dados que se queira ter uma visão geral de relacionamentos entre as variáveis (ou colunas de um dataframe. Para isso um pairplot faz o cruzamento entre todas as variáveis. O método seaborn.pairplot(), por default, cria uma matriz de Axes comparando aos pares as variáveis numéricas do dataframe usado como fonte de dados. Na diagonal dessa matriz uma distribuição univariada é exibida para mostrar a distribuição dos dados em cada coluna.

» sns.pairplot(df2007[['gdpPercap', 'lifeExp']],
»              diag_kind='kde', plot_kws={'color':'r','alpha': .9})


O parâmetro plot_kws recebe um dicionário de propriedades com valores.

Outro método é seaborn.catplot() que traz diversas funcionalidades para representar relações entre variáveis numéricas ou categóricas. Para uma amostra criamos um dataframe com valores de uma parábola e um seno.

» dfGraf=pd.DataFrame(np.arange(20), columns = ['x'])
» dfGraf['quadrado']=dfGraf['x']**2
» dfGraf['seno']=np.sin(dfGraf['x'])

» sns.catplot(x='x', y='quadrado', kind='strip', data=dfGraf)      # plota a figura 35
» sns.catplot(x='x', y='seno', kind='bar', data=dfGraf)            # plota a figura 36

Bibliografia

Todos os sites acessados em setembro de 2021.

Consulte bibliografia completa em Pandas, Introdução neste site.

Dataframes: multi-índices e concatenção

Índices Hierárquicos

É possível criar series e dataframes com índices e subíndices. Esse processo de indexação hierárquica é importante para a reformatação (reshaping ), formação de tabelas pivot e outras operações de agrupamento de dados.

» import pandas as pd
» import numpy as np

» # formamos uma series com índices duplos 
» sr = pd.Series([11, 12, 21, 22, 23, 31, 32, 41, 42],
                 index=[['A', 'A', 'B', 'B', 'B', 'C', 'C', 'D', 'D'],
                 [1, 2, 1, 2, 3, 1, 2, 1, 2]])
» sr
↳ A  1    11
     2    12
  B  1    21
     2    22
     3    23
  C  1    31
     2    32
  D  1    41
     2    42

» # essa series possui índices
» sr.index
↳ MultiIndex([('A', 1), ('A', 2),
              ('B', 1), ('B', 2), ('B', 3),
              ('C', 1), ('C', 2),
              ('D', 1), ('D', 2)],)

» # da mesma forma podemos transformar essa series um um dataframe
» df = pd.DataFrame(sr)

» # Os índices do dataframe são os mesmos: df.index

» # o índice B corresponde à 3 linhas
» df.loc['B']
↳        0
  1     21
  2     22
  3     23

» df.loc['B'].loc[2]
↳ 0    22

» # idem para a series
» sr['C']
↳ 1    31
  2    32

» sr['C'][1]
↳ 31

» # podemos listar as linhas de 'A' até 'C'
» sr['A':'C']
↳ A  1    11
     2    12
  B  1    21
     2    22
     3    23
  C  1    31
     2    32

» # ou as linhas correspondentes à 'A' e 'C'
» sr.loc[['A','C']]
↳ A  1    11
     2    12
  C  1    31
     2    32

» # seleção pelo índice interno pode feita diretamente
» sr.loc[:, 2]
↳ A    12
  B    22
  C    32
  D    42

» sr.loc[:, 3]
↳ B    23

stack() e unstack()

Os dados de uma series com índices hierárquicos podem ser rearranjados em um DataFrame com o uso de método Series.unstack(). Os índices internos se tornam nomes das colunas. Valores não existentes, como o correspondende aos índices A, 3, são preenchidos com NaN.

» df = sr.unstack()
» df
↳          1         2       3
  A     11.0     12.0      NaN
  B     21.0     22.0     23.0
  C     31.0     32.0      NaN
  D     41.0     42.0      NaN

» # para retornar à uma series
» df.unstack()
↳ 1  A    11.0
     B    21.0
     C    31.0
     D    41.0
  2  A    12.0
     B    22.0
     C    32.0
     D    42.0
  3  A     NaN
     B    23.0
     C     NaN
     D     NaN

No processo de desempilhar o dataframe (unstack ) os nomes das colunas foram usados como índices primários.

Um dataframe pode ter índices hierarquizados para linhas e colunas.

» clima = np.array([[25,20,30],[20,16,15],[15,25,27],[40,60,78]])
» dfClima = pd.DataFrame(clima,
»                       index=[['Temperatura','Temperatura','Umidade','Umidade'],
»                              ['dia','noite','dia','noite']],
»                       columns=[['Paraná','Paraná','Amazonas'],['Cascavel','Curitiba','Manaus']]
»                       )

» # inserindo nomes para as linhas e colunas
» dfClima.index.names = ['Característica', 'D/N']        # D/N = dia/noite
» dfClima.columns.names = ['Estado', 'Cidade']

» # o resultado é
» dfClima
↳                  Estado                   Paraná     Amazonas
                   Cidade     Cascavel     Curitiba      Manaus
  Característica      D/N             
  Temperatura         dia           25           20          30
                    noite           20           16          15
  Umidade             dia           15           25          27
                    noite           40           60          78

Se o processo de criação de dataframes com os mesmos índices será repetido várias vezes ,pode ser útil definir previamente os objetos multindexes.

» colunas = pd.MultiIndex.from_arrays([['Paraná', 'Paraná', 'Amazonas'],
»                                      ['Cascavel', 'Curitiba', 'Manaus']],
»                                      names=['Estado', 'Cidade'])

» linhas = pd.MultiIndex.from_arrays([['Temperatura','Temperatura','Umidade','Umidade'],
»                                     ['dia','noite','dia','noite']],
»                                     names=['Característica', 'D/N'])

» linhas
↳ MultiIndex([('Temperatura',   'dia'),
              ('Temperatura', 'noite'),
              (    'Umidade',   'dia'),
              (    'Umidade', 'noite')],
             names=['Característica', 'D/N'])

» pd.DataFrame(clima, index=linhas, columns=colunas)
↳                 Estado     Paraná             Amazonas
                  Cidade   Cascavel    Curitiba   Manaus
  Característica     D/N
  Temperatura        dia         25          20       30
                   noite         20          16       15
  Umidade            dia         15          25       27
                   noite         40          60       78

swaplevel() e groupby()

O ordenamento dos níveis nos dataframes pode ser alterado com o método dataframe.swaplevel(indice1, indice2). Índices primários podem ser permutados com índice secundários. Com dataframe.sort_index(level=n) podemos ordenar as linhas do dataframe segundo os nomes dos índices do nível n.

» dfClima.swaplevel('D/N', 'Característica')
↳                 Estado     Paraná               Amazonas
                  Cidade   Cascavel    Curitiba     Manaus
  D/N     Característica
  dia        Temperatura         25          20         30
  noite      Temperatura         20          16         15
  dia            Umidade         15          25         27
  noite          Umidade         40          60         78

» # ordenando as linhas pelos labels do nível 1 (D/N)
» dfClima.sort_index(level=1)

↳                  Estado                    Paraná    Amazonas
                   Cidade     Cascavel     Curitiba      Manaus
  Característica      D/N
  Temperatura         dia           25           20          30
  Umidade             dia           15           25          27
  Temperatura       noite           20           16          15
  Umidade           noite           40           60          78

» # alternativamente podemos inverter a ordem dos níveis e ordenar pelo nivel 0
» dfClima.swaplevel(0, 1).sort_index(level=0)
↳                 Estado                    Paraná    Amazonas
                  Cidade     Cascavel     Curitiba      Manaus
  D/N     Característica
  dia        Temperatura           25           20          30
                 Umidade           15           25          27
  noite      Temperatura           20           16          15
                 Umidade           40           60          78

» # soma dos valores agrupados pelo nível 1 (D/N)
» dfClima.groupby(level=1).sum()
 
↳ Estado                    Paraná    Amazonas
  Cidade     Cascavel     Curitiba      Manaus
  D/N 
  dia              40           45          57
  noite            60           76          93

O método dataframe.groupby(), que veremos mais tarde com maiores detalhes, permite o agrupamento dos dados de um determinado índice (ou nível de índices). Por ex., dataframe.groupby(level=n).sum() faz o agrupamento dos dados segundo o n-ésimo nível de índice e depois soma esses valores. Muitas outras funções estatísticas ficam disponíveis com agrupamentos por groupby.

» # soma dos valores agrupados pelo nível 0
» dfClima.groupby(level='Característica').mean()
↳                  Estado               Paraná    Amazonas
                   Cidade  Cascavel   Curitiba      Manaus
  Característica
  Temperatura                  22.5       18.0        22.5
  Umidade                      27.5       42.5        52.5

» # a média dos valores agrupados pelo índice D/N
» dfClima.groupby(level=0).mean()

↳                   Estado              Paraná   Amazonas
                    Cidade  Cascavel  Curitiba     Manaus
  Característica
  Temperatura                   22.5      18.0       22.5
  Umidade                       27.5      42.5       52.5

» # o valor máximo agrupado pelo nível 'Característica'
» dfClima.groupby(level='Característica').max()
↳                  Estado              Paraná    Amazonas
  Cidade                   Cascavel   Curitiba     Manaus
  Característica
  Temperatura                    25         20         30
  Umidade                        40         60         78

Vimos previamente que qualquer coluna pode ser transformada em índice do dataframe. Mais de uma coluna pode também ser usada: para isso usamos dataframe.set_index([coluna1, coluna2]). Por default essa operação coloca coluna1, coluna2 como índices e descarta as colunas usadas. Para alterar esse comportamento (e manter as colunas) usamos o parâmetro drop=False. O método dataframe.reset_index() remove os índices colocando-os como colunas e criando um novo conjunto de índices.

» # criamos um dataframe arbitrário
» dfNums = pd.DataFrame({'a': range(1,6),
»                        'texto-a': ['um','dois','três','quatro','cinco'],
»                        'b': range(5, 0, -1),
»                        'texto-b': ['cinco', 'quatro','três','dois','um']
»                       })

» # dataframe inicial
» dfNums
↳      a     texto-a     b     texto-b
  0    1          um     5       cinco
  1    2        dois     4      quatro
  2    3        três     3        três
  3    4      quatro     2        dois
  4    5       cinco     1          um

» # usamos as colunas 'a' e 'b' como índices
» dfNums2 = dfNums.set_index(['a', 'b'])

» dfNums2
↳          texto-a    texto-b
  a    b         
  1    5        um      cinco
  2    4      dois     quatro
  3    3      três       três
  4    2    quatro       dois
  5    1     cinco         um

» # para descartar os índices (e recuperar as colunas)
» dfNums2.reset_index()

↳       a     b     texto-a     texto-b
  0     1     5     um          cinco
  1     2     4     dois        quatro
  2     3     3     três        três
  3     4     2     quatro      dois
  4     5     1     cinco       um

» # podemos usar as colunas 'a' e 'texto-a' como índices sem descartar essas colunas
» dfNums.set_index(['a', 'texto-a'], drop=False)
↳                 a     texto-a    b    texto-b
  a     texto-a                 
  1     um        1     um         5      cinco
  2     dois      2     dois       4     quatro
  3     três      3     três       3       três
  4     quatro    4     quatro     2       dois
  5     cinco     5     cinco      1         um

Uma exceção é lançada se já existem colunas com os mesmos nomes recuperados por reset_index.

Combinando dataframes

Podemos juntar dataframes de várias formas. pandas.merge() junta dataframes usando um ou mais índices, em operações semelhantes àquelas de bancos de dados relacionais usando-se as operações de join do SQL. pandas.concat() faz a concatenação ou empilhamento dos dataframes ao longo do eixo escolhido. pandas.combine_first() permite a junção de dados que se superpõe (existem em mais de uma tabela), preenchendo valores ausentes um uma tabela com aqueles em outra tabela fornecida.

merge()

df1.merge(df2) retorna outro dataframe que é a junção dos dois dataframes. O método possui a seguinte assinatura:
df1.merge(df2, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=False, suffixes=('_x', '_y'), copy=True, indicator=False, validate=None)

A junção pode ser feita sobre nomes das colunas ou índices. Uma Series nomeada é tratada como um dataframe de coluna única. São parâmetros:

df1, df2 dataframe ou Series nomeada. Junção de df1 com df2
how tipo de junção: left, right, outer, inner, cross:
inner: usa apenas combinações de chaves existentes em ambas as tabelas preserva ordem das chaves.
outer: usa todas as combinações de chaves em cada uma das tabelas,
left: usa todas as combinações de chaves existentes na tabela à esquerda,
right: usa todas as combinações de chaves existentes na tabela à direita,
cross: cria o produto cartesiano das tabelas, preserva ordem dos índices.
on coluna ou índice para a junção. Deve existir em ambos os dataframes
left_on nome da coluna ou índice (ou lista) em df1.
right_on nome da coluna ou índice (ou lista) em df2.
left_index False/True: use o índice de df1 como chave.
right_index False/True: use o índice de df2 como chave.
sort False/True: Ordena os índices no resultado.
suffixes lista: default = (“_x”, “_y”). Sufixos para índices de mesmo nome
copy False/True: Se False evita a cópia, se possível
indicator False/True ou str: Se True acrescenta coluna “_merge” com informações sobre as linhas.
validate str, opcional. Se especificada verifica se a junção é do tipo:
one_to_one ou 1:1 : se chave da fusão é única nos dois dataframes,
one_to_many ou 1:m : se chave da fusão é única em df1 (lado esquerdo),
many_to_one ou m:1 : se chave da fusão é única em df2 (lado direito),
many_to_many ou m:m : embora permitida não resulta em nenhuma verificação.

Comparação de how='' com comandos SQL: (Pandas e SQL comparados).

how= similar ao SQL
left left outer join. Preserva ordem das chaves.
right right outer join. Preserva ordem das chaves.
outer full outer join. Ordena por nomes das chaves.
inner inner join. Preserva ordem das chaves à esquerda.
» # criando dataframes 
» df1 = pd.DataFrame({'chave': ['a', 'a', 'a', 'b', 'b', 'b', 'c'], 'data1': range(7)})
» df2 = pd.DataFrame({'chave': ['a', 'b', 'd'], 'data2': range(3)})

» # exibindo df1, df2 e sua junção com merge
» display(df1, df2, pd.merge(df1, df2))

↳    chave   data1
  0      a       0
  1      a       1
  2      a       2
  3      b       3
  4      b       4
  5      b       5
  6      c       6

↳    chave   data2
  0      a      0
  1      b      1
  2      d      2

↳    chave   data1   data2
  0      a       0       0
  1      a       1       0
  2      a       2       0
  3      b       3       1
  4      b       4       1
  5      b       5       1


Como os dois dataframes possuem uma coluna com nome comum a junção foi feita com base nos valores da coluna com esse nome. Essa informação pode ser explicitada com pd.merge(df1, df2, on='chave').

Se os nomes das colunas de cada dataframe for diferente eles devem ser definidos com os parâmetros left_on, right_on.

» df3 = pd.DataFrame({'chave1': ['a', 'a', 'a', 'b', 'b', 'c', 'd'], 'data1': range(7)})
» df4 = pd.DataFrame({'chave2': ['a', 'b', 'd'], 'data2': range(3)})

» display(df3, df4, pd.merge(df3, df4, left_on='chave1', right_on='chave2'))
↳    chave1     data1
  0       a     0
  1       a     1
  2       a     2
  3       b     3
  4       b     4
  5       c     5
  6       d     6

↳    chave2     data2
  0       a     0
  1       b     1
  2       d     2

↳    chave1     data1     chave2     data2
  0       a     0         a          0
  1       a     1         a          0
  2       a     2         a          0
  3       b     3         b          1
  4       b     4         b          1
  5       d     6         d          2

Vemos na concatenação acima que o método usado reune apenas valores existentes nas duas tabelas. Isso é equivalente a passar o parâmetro how=’inner’ (um inner join ). Outra opção consiste em fazer o ligamento externo.

» # para conseguir um outer join    
» pd.merge(df1, df2, how='outer')

↳     chave   data1   data2
  0     a       0.0     0.0
  1     a       1.0     0.0
  2     a       2.0     0.0
  3     b       3.0     1.0
  4     b       4.0     1.0
  5     b       5.0     1.0
  6     c       6.0     NaN
  7     d       NaN     2.0

» dd.merge(df1, df2, how='left')

↳   chave   data1   data2
  0     a       0     0.0
  1     a       1     0.0
  2     a       2     0.0
  3     b       3     1.0
  4     b       4     1.0
  5     b       5     1.0
  6     c       6     NaN

» pd.merge(df1, df2, how='right')

↳   chave     data1   data2
  0     a       0.0       0
  1     a       1.0       0
  2     a       2.0       0
  3     b       3.0       1
  4     b       4.0       1
  5     b       5.0       1
  6     d       NaN       2

Tabelas podem ser ligadas por mais de uma chave, quando os dataframes possuem índices hierarquizados. As chaves são usadas como se fossem uma única chave concatenada.

» df1 = pd.DataFrame({'chave_1': ['rato', 'rato', 'gato'],
»                      'chave_2': ['Jones', 'Jerry', 'Tom'],
»                      'valor_A': [10, 20, 30]})
» df2 = pd.DataFrame({'chave_1': ['rato', 'rato', 'gato', 'gato'],
»                       'chave_2': ['Jones', 'Jerry', 'Tom', 'Tim'],
»                       'valor_B': [40, 50, 60, 70]})
                      
» # exibindo os dataframes e a junção externa em duas chaves
» display(df1, df2, pd.merge(df1, df2, on=['chave_1','chave_2'], how='outer'))

↳     chave_1     chave_2    valor_A
  0      rato       Jones         10
  1      rato       Jerry         20
  2      gato         Tom         30

↳    chave_1     chave_2   valor_B
  0     rato       Jones        40
  1     rato       Jerry        50
  2     gato         Tom        60
  3     gato         Tim        70

↳     chave_1    chave_2    valor_A    valor_B
  0     rato       Jones       10.0         40
  1     rato       Jerry       20.0         50
  2     gato        Tom        30.0         60
  3     gato        Tim         NaN         70

» # a junção interna em duas chaves
» pd.merge(df1, df2, on=['chave_1','chave_2'], how='inner')

↳     chave_1   chave_2    valor_A    valor_B
  0      rato     Jones         10         40
  1      rato     Jerry         20         50
  2      gato      Tom          30         60

Se a junção for feita sobre campos (nomes de colunas) com o mesmo nome estes serão alterados para continuar a representar suas colunas de origem. No caso do exemplo as colunas com nome valor foram renomeadas para valor_x e valor_y.

» df1 = pd.DataFrame({'chave': ['a', 'b', 'c'], 'valor': [1,2,3]})
» df2 = pd.DataFrame({'chave': ['a', 'b', 'c'], 'valor': [10,20,30]})

» mrg = pd.merge(df1, df2, on='chave')

» display(df1, df2, mrg)

↳    chave  valor
  0      a      1
  1      b      2
  2      c      3

↳    chave  valor
  0      a     10
  1      b     20
  2      c     30

↳    chave  valor_x  valor_y
  0      a        1       10
  1      b        2       20
  2      c        3       30

A chave usada na fusão (merge) pode estar no índice de um ou ambas as tabelas. No exemplo usamos pd.merge(esquerda, direita, left_on='chave', right_index=True) que faz a junção de esquerda.chave com direita.index

» esquerda = pd.DataFrame({'chave': ['a1', 'a1', 'a2', 'a1', 'a2', 'a3'], 'valor_1': range(6)})
» direita = pd.DataFrame({'valor_2': [50, 70]}, index=['a1', 'a2'])

» mrg = pd.merge(esquerda, direita, left_on='chave', right_index=True)

» # exibindo dataframes e sua junção, usando o índice da tabela à direita
» display(esquerda, direita, mrg)

↳    chave   valor_1
  0     a1         0
  1     a1         1
  2     a2         2
  3     a1         3
  4     a2         4
  5     a3         5

↳    valor_2
  a1      50
  a2      70

↳    chave   valor_1    valor_2
  0     a1         0         50
  1     a1         1         50
  3     a1         3         50
  2     a2         2         70
  4     a2         4         70

» # se os dataframes forem invertidos conseguiríamos o
» # mesmo resultado, exceto pela ordem das colunas, usando:
» # pd.merge(direita, esquerda, right_on='chave', left_index=True)

Junções com join()

Junções podem ser feitas com dataframe.join(dfOutro) que, por default, faz a união outer join usando o índice como chave. Esse método tem a seguinte assinatura, onde os parâmetros são
dataframe.join(dfOutro, on, how, lsuffix, rsuffix, sort),
Todos os parâmetros são opcionais exceto dfOutro. Os defaults estão em negrito.

dfOutro DataFrame, Series ou lista de DataFrames.
on string, especifica em que chave(s) fazer a junção
how strings: left, right, outer, inner. Especifica o tipo de junção.
lsuffix/rsuffix Default = ”. String a concatenar à esquerda/direita em colunas com mesmo nome.
sort False/True. Se True ordena o dataframe pela chave de junção.
» # dataframe join
» df1 = pd.DataFrame({'nome': ['Paulo', 'Maria', 'Julio','Marta'],
                       'idade': [35, 43, 31, 56]})
» df2 = pd.DataFrame({'profissao': ['médico', 'engenheiro', 'advogado']})

» df1
↳      nome      idade
  0    Paulo     35
  1    Maria     43
  2    Julio     31
  3    Marta     56

» df2
↳      profissao
  0    médico
  1    engenheiro
  2    advogado

» df1.join(df2, on=df1.index,  lsuffix='_1', rsuffix='_2') # , how = 'left' (default)
↳      nome   idade_1    profissao    idade_2
  0   Paulo        35       médico       35.0
  1   Maria        43   engenheiro       40.0
  2   Julio        31     advogado       31.0
  3   Marta        56          NaN        NaN

» # um inner join
» df1.join(df2, lsuffix='_', how='inner')
↳      nome    idade_    profissao   idade
  0   Paulo       35        médico      35
  1   Maria       43    engenheiro      40
  2   Julio       31      advogado      31

Vários dataframes podem ser concatenados de uma vez. Para isso eles devem ter dimensões compatíveis.

» # Vários dataframes podem ser concatenados
» df1 = pd.DataFrame([[23, 83], [93, 10], [73, 89], [68, 90]],
»                    index=['a', 'b', 'e', 'f'],
»                    columns=['A', 'B'])

» df2 = pd.DataFrame([[2, 8], [9, 1], [7, 8], [6, 9]],
»                    index=['a', 'b', 'c', 'd'],
»                    columns=['C', 'D'])

» df3 = pd.DataFrame([[3, 3], [3, 0], [3, 9], [8, 0]],
»                    index=['a', 'c', 'd', 'e'],
»                    columns=['E', 'F'])

» # exibe os 3 dataframes
» display(df1, df2, df3)

↳ A    B
  a    23    83
  b    93    10
  e    73    89
  f    68    90

↳ C    D
  a    2    8
  b    9    1
  c    7    8
  d    6    9
  
↳ E    F
  a    3    3
  c    3    0
  d    3    9
  e    8    0

» # exibe a junção dos dataframes
» df1.join([df2, df3])

↳         A       B      C      D      E      F
  a    23.0    83.0    2.0    8.0    3.0    3.0
  b    93.0    10.0    9.0    1.0    NaN    NaN
  e    73.0    89.0    NaN    NaN    8.0    0.0
  f    68.0    90.0    NaN    NaN    NaN    NaN

Como sempre, campos não fornecidos são preenchidos por NaN. Por ex.: df1.join([df2, df3]).loc['f', 'F'] = NaN.

concatenate()

Podemos concatenar numpy.arrays, Series e dataframes ao longo do eixo desejado.

» # Concatenando um array ao longo de um eixo
» # criamos 2 arrays
» arr1 = np.arange(6).reshape((3, 2))

» arr1
↳ array([[0, 1],
         [2, 3],
         [4, 5]])

» # concatenando arr1 consigo mesmo, ao longo de colunas
» np.concatenate([arr1, arr1], axis=1)
↳ array([[0, 1, 0, 1],
         [2, 3, 2, 3],
         [4, 5, 4, 5]])

» # concatenando arr1 consigo mesmo, ao longo de linhas
» np.concatenate([arr1, arr1], axis=0)
↳ array([[0, 1],
         [2, 3],
         [4, 5],
         [0, 1],
         [2, 3],
         [4, 5]])

» # defina outro array, com shape (3, 1)
» arr2 = np.array([[0], [1], [2]])

» arr2
↳ array([[0],
         [1],
         [2]])

» # concatenando arr1 2 arr2 pelas colunas
» np.concatenate([arr1, arr2], axis=1)

↳ array([[0, 1, 0],
         [2, 3, 1],
         [4, 5, 2]])

» # (tentando) concatenar arr1 2 arr2 pelas linhas
» np.concatenate([arr1, arr2], axis=0)
↳ ValueError: all the input array dimensions for the concatenation axis must match exactly,
  but along dimension 1, the array at index 0 has size 2 and the array at index 1 has size 1


Vemos que podemos concatenar uma matriz coluna (3 × 1) com outra matriz (3 × 2) pelas colunas, mas não pelas linhas pois as dimensãos são incompatíveis.

combine() e combine_first()

O método df1.combine(df2, func, fill_value=None, overwrite=True) combina df1 e df2, coluna a coluna, aplicando func para decidir qual valor será usado.

Podemos criar uma função que receba duas colunas e realize alguma operação entre elas, retornando outra coluna. No ex., a função f faz a soma dos elementos de duas colunas e retorna aquela com menor soma. A função g seleciona, a cada linha, qual é o maior elemento. Quando o parâmetro fill_value=r é usado todos os valores NaN são substituídos por r antes de serem submetidos à função func, exceto se ambos os valores forem nulos, quando não existirá substituição.

» df1 = pd.DataFrame({'A': [0, 3], 'B': [7, 2]})
» df2 = pd.DataFrame({'A': [2, 6], 'B': [1, 3]})

» df1
↳      A    B
  0    0    7
  1    3    2

» df2
↳      A    B
  0    2    1
  1    6    3

» # a função de comparação pode ser
» def f(x,y):
»     if x.sum() < y.sum():
»         return x
»     else:
»         return y

» # a combinação, usando essa função
» df1.combine(df2, f)
↳      A    B
  0    0    1
  1    3    3

» # O mesmo resultado pode ser obtido com uma função lambda
» df1.combine(df2, lambda x, y: x if x.sum() < y.sum() else y)

» # funções mais complexas podem ser usadas
» df1.combine(df2, lambda x, y: (x+y)*(y-x))
↳       A     B
  0     4   -48
  1    27     5

» # outro exemplo, selecionar o maior elemento de cada df
» def g(x,y):
»     a = x[0] if x[0] > y[0] else y[0]
»     b = x[1] if x[1] > y[1] else y[1]
»     return pd.Series([a,b])

» df1.combine(df2,g)
↳      A    B
  0    2    7
  1    6    3

» # o mesmo poderia ser feito com uma funlão lambda
» maior = lambda x,y: pd.Series([x[0] if x[0] > y[0] else y[0],
                                x[1] if x[1] > y[1] else y[1]])
» df1.combine(df2,maior) # mesmo output
 
» # uso de fill_value
» df1 = pd.DataFrame({'A': [0, 0], 'B': [np.NaN, 4]})
» df2 = pd.DataFrame({'A': [1, 1], 'B': [3, 3]})

» df1.combine(df2, maior, fill_value=6)
↳      A      B
  0    1    6.0
  1    1    4.0

Já o método dataframe.combine_first(dfOutro) substitui os valores NaN no dataframe com os valores de dfOutro, quando esses valores existirem.

» df1 = pd.DataFrame({'a': [1, np.nan, 5, np.nan],
»                     'b': [np.nan, 2, np.nan, 6],
»                     'c': range(2, 18, 4)})
» df2 = pd.DataFrame({'a': [5, 4, np.nan, 3, 7],
»                     'b': [np.nan, 3, 4, 6, 8]})
» display(df1, df2)
↳        a      b     c
  0    1.0    NaN     2
  1    NaN    2.0     6
  2    5.0    NaN    10
  3    NaN    6.0    14
  
↳        a      b
  0    5.0    NaN
  1    4.0    3.0
  2    NaN    4.0
  3    3.0    6.0

» df1.combine_first(df2)
↳        a      b      c
  0    1.0    NaN    2.0
  1    4.0    2.0    6.0
  2    5.0    4.0   10.0
  3    3.0    6.0   14.0
  4    7.0    8.0    NaN

Bibliografia

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

Dataframes, preparação de dados


Preparação de dados

Programadores que lidam com análise de dados passam grande parte do tempo dedicado a um projeto preparando esses dados, antes mesmo de começar qualquer análise. Normalmente os dados são importados de uma fonte externa, tal como um arquivo em forma tabular em html, pdf, texto puro ou csv. Eles precisam ser convertidos para um formato legível e muitas vezes contém erros e valores ausentes. A vezes o próprio processo de conversão introduz perda de dados, tal como acontece em textos impressos transformados em texto digital por OCR (optical character recognition ). Seja qual for a origem dos dados algum trabalho de depuração deve ser feito. Em seguida eles devem passar por formatação adequada, a quebra de tabelas, o estabelecimento de vínculos entre elas, etc. Pandas oferece boas ferramentas para todas essas etapas.

Dados ausentes

Já vimos que dados não presentes em alguma tabela são representados por NaN (not a number). O objeto None do python também é tratado como um valor ausente ou NA (not available). O método dropna() descarta linhas (se axis=0, default) ou colunas (se axis=1) contendo campos nulos. dropna(how='all') descarta linhas ou colunas se todos os campos forem nulos. Também podemos determinar que apenas linhas ou colunas com um número mínimo de elementos não nulos sejam mantidas, com df.dropna(thresh=n).

» import pandas as pd
» import numpy as np

» dados = pd.Series([121.45, np.nan ,32.12,42.21,51.56])
» dados
↳ 0    121.45
  1       NaN
  2     32.12
  3     42.21
  4     51.56

» dados[3]= None
» dados.isnull()
↳ 0    False
  1     True
  2    False
  3     True
  4    False

» dados.dropna()                   # o mesmo que dados[dados.notnull()]
↳ 0    121.45
  2     32.12
  4     51.56

» from numpy import nan as NA     # para estabelecer um alias curto para np.nan
» data = pd.DataFrame([[1., 6.5, 3.9], [1.3, NA, NA], [NA, NA, NA], [NA, 5.8, 6.7]])
» data
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  2    NaN    NaN    NaN
  3    NaN    5.8    6.7

» data.dropna()
↳        0      1      2
  0    1.0    6.5    3.9

» data.dropna(how='all')
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  3    NaN    5.8    6.7

» data[4] = NA
» data
↳        0      1      2      4
  0    1.0    6.5    3.9    NaN
  1    1.3    NaN    NaN    NaN
  2    NaN    NaN    NaN    NaN
  3    NaN    5.8    6.7    NaN

» data.dropna(axis=1, how='all')
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  2    NaN    NaN    NaN
  3    NaN    5.8    6.7

Preenchendo valores ausentes

A invés de descartar linhas e colunas com campos ausentes podemos preencher estas lacunas. df.fillna(const) substitui campos NA com o valor único const. Um dicionário {coluna:valor} pode ser passado contendo constantes diferentes para cada coluna. Observando que df.mean() retorna uma Series com as médias de cada colunas, podemos usar df.fillna(df.mean()) para preencer NAs de cada coluna com essa média. Também podemos passar o parâmetro df.fillna(method='ffill') para preencher cada NA com o valor que o antecede na coluna. df.fillna(method='bfill') preenche NAs com o valor que o segue.

» # criando um df de teste com campos NA
» df = pd.DataFrame(np.random.randn(4, 3))
» df.iloc[0:3, 1] = NA
» df.iloc[1:3, 2] = NA

» df
↳             0           1            2
  0    0.615016         NaN    -0.860821
  1    1.195041         NaN          NaN
  2   -0.110482         NaN          NaN
  3    1.837690    1.569459     0.891858

» # preenche NAs com 0
» df.fillna(0)
↳             0           1           2
  0    0.615016    0.000000   -0.860821
  1    1.195041    0.000000    0.000000
  2   -0.110482    0.000000    0.000000
  3    1.837690    1.569459    0.891858

» # preenche coluna 1 com 10, coluna 2 com 20
» df.fillna({1:10, 2:20})
↳             0            1            2
  0    0.615016    10.000000    -0.860821
  1    1.195041    10.000000    20.000000
  2   -0.110482    10.000000    20.000000
  3    1.837690     1.569459     0.891858

» df.fillna(method='ffill')
↳             0          1            2
  0    0.615016        NaN    -0.860821
  1    1.195041        NaN    -0.860821
  2   -0.110482        NaN    -0.860821
  3    1.837690   1.569459     0.891858

» df.fillna(method='bfill')
↳             0           1           2
  0    0.615016    1.569459   -0.860821
  1    1.195041    1.569459    0.891858
  2   -0.110482    1.569459    0.891858
  3    1.837690    1.569459    0.891858

» df.fillna(method='bfill', limit=2)
» df.mean()
↳ 0    0.884316
  1    1.569459
  2    0.015519

» df.fillna(df.mean())
↳             0           1           2
  0    0.615016         NaN   -0.860821
  1    1.195041    1.569459    0.891858
  2   -0.110482    1.569459    0.891858
  3    1.837690    1.569459    0.891858

Vemos que df.fillna(method='ffill') não substituiu valores nas linhas 0, 1, 2 da coluna 1 pois nenhum valor os antecede. Nesse caso teríamos que usar method='bfill', ou outra forma de preencher o campo vazio.

Substituições com dataframe.replace()

O método df.replace() substitui valores específicos em uma Series ou dataframe. Por ex., suponha que temos uma Series de valores positivos e a inserção de negativos foi convencionada para indicar valores ausentes. Podemos alterar esses valores usando df.replace(), lembrando que nenhuma das formas abaixo altera a Serie original, a menos que inplace=True seja usado.

» serie = pd.Series([12,-2, 34, -1])
» serie
↳ 0    12
  1    -2
  2    34
  3    -1

» serie.replace(-2, -90)
↳ 0    12
  1   -90
  2    34
  3    -1

» serie.replace([-2,-1], [20,10])
↳ 0    12
  1    20
  2    34
  3    10

» serie.replace(-1, NA)
↳ 0    12.0
  1    -2.0
  2    34.0
  3     NaN

Claro que df.replace() pode ser usado para substituir um valor específico por valores calculados, usando métodos mais sofisticados de avaliação.

Em um dataframe df.replace(lista1, lista2) pode ser usado para substituir valores da lista1 pelos da lista2 (que deve ter o mesmo tamanho). df.replace(lista, escalar) substitui todos os valores em lista pelo escalar e df.replace(dicionario) substitui as chaves pelas valores no dicionário.

» df = pd.DataFrame({'a':[9,56,67], 'b':[33,55,66], 'c':[63,69,67], 'd':[2,3,9]})
» df
↳       a     b     c    d
  0     9    33    63    2
  1    56    55    69    3
  2    67    66    67    9

» df.replace(9, 100)
↳        a     b     c     d
  0    100    33    63     2
  1     56    55    69     3
  2     67    66    67   100

» df.replace([9, 55, 67], 0)
↳      a     b     c    d
  0    0    33    63    2
  1   56     0    69    3
  2    0    66     0    0

» df.replace([9, 55, 67], [1,2,3])
↳      a     b     c    d
  0    1    33    63    2
  1   56     2    69    3
  2    3    66     3    1

» df.replace({9:-9, 33:-33})
↳       a      b     c    d
  0    -9    -33    63    2
  1    56     55    69    3
  2    67     66    67   -9

Análise de outliers

Em qualquer processo de tomada de medidas ou coleta de dados existem restrições à precisão obtida. Mas, além da precisão restrita, é frequente existirem dados muito fora de qualquer curva esperada. Esses são os chamados pontos fora da curva ou outliers e geralmente são descartados. Os critérios de decisão sobre quais pontos são outliers dependem do modelo que se quer tratar.

No pandas podemos encontrar valores que estão acima ou abaixo de um certo limite.

Lembrando que np.random.randn(M, p) retorna um array de p colunas, cada uma com M valores, retirados aleatoriamente de uma distribuição normal com média 0 e variância 1, começamos por coletar um dataframe para testes.

Considerando os máximos e mínimos exibidos, vamos estabelecer arbitrariamente que valores afastados acima de 3 da média do conjunto são outliers. Isso quer dizer que consideraremos os pontos com |x| > 3 como outliers (onde |x| significa valor absoluto de x). Uma das possibilidades consiste em substituir valores não aceitáveis por np.nan e depois usar uma das formas de fill para preencher esses campos.

» dados = pd.DataFrame(np.random.randn(1000, 4))
» # são os valores mínimo e máximo desse dataframe
» dados.min().min(), dados.max().max()
↳ (-3.7113843289590496, 3.480659301328407)

» # substituimos |x| > 3 por np.nan
» dados[np.abs(dados) > 3] = np.nan
» dados.describe()    # (1) visualização do dataframe (alguns campos exibidos)
↳                   0             1             2            3
  count    996.000000    997.000000    998.000000   996.000000
  mean       0.086548      0.021479     -0.046291     0.019611
  min       -2.772219     -2.860741     -2.763174    -2.644022
  max        2.763864      2.849207      2.955914     2.905516

» dados = dados.fillna(method='bfill')
» dados.describe()    # (2) visualização do dataframe (alguns campos exibidos)
↳                    0              1              2              3
  count    1000.000000    1000.000000    1000.000000    1000.000000
  mean        0.089292       0.021845      -0.046568       0.020410
  min        -2.772219      -2.860741      -2.763174      -2.644022
  max         2.763864       2.849207       2.955914       2.905516

No primeiro uso de describe a contagem count mostra que existem linhas com campos nulos para cada coluna. Após a operação de fill todos os campos são numéricos.

Removendo linhas duplicadas

Para remover linhas duplicadas em um dataframe usamos df.drop_duplicates(). Valores duplicados em apenas uma coluna podem ser removidos com df.drop_duplicates('nomeColuna'), ou em várias colunas, passando-se uma lista df.drop_duplicates(['col1',..., 'coln']). Por default a primeira linhas, entre as duplicadas é mantida. Para manter a última usamos df.drop_duplicates('coluna', keep='last').

» # remoção de linhas duplicadas
» dic ={'col1': ['vaca', 'vaca', 'pato','pato'], 'col2': [1, 3, 4, 4]} 
» df = pd.dfFrame(dic)
» df
↳      col1    col2
  0    vaca    1
  1    vaca    3
  2    pato    4
  3    pato    4

» df.duplicated()           # retorna uma Series mostrando linhas duplicadas
↳ 0    False
  1    False
  2    False
  3     True

» df.drop_duplicates()
↳      col1   col2
  0    vaca      1
  1    vaca      3
  2    pato      4

» df.drop_duplicates('col1')
↳      col1  col2
  0    vaca     1
  2    pato     4

» df.drop_duplicates('col1', keep='last')
↳      col1   col2
  1    vaca     3
  3    pato     4

No atual estado de Pandas não é possível fazer a remoção de duplicadas sobre colunas. Para isso obtenha a transposta do dataframe, remova linhas duplicadas e o transponha novamente.

Transformações sobre elementos de um dataframe

Um restaurante faz uma lista de aquisição de produtos, descrevendo o ítem e quantas unidades devem se adquiridas.

» compra = {'produto':['leite', 'manteiga', 'laranja', 'arroz'],
            'quantos':[15, 40,50, 30]} 
» dfComprar = pd.DataFrame(compra)
» dfComprar
↳      produto  quantos
  0      leite       15
  1   manteiga       40
  2    laranja       50
  3      arroz       30

Mais tarde o gerente pede que os produtos sejam classificados como veganos ou não. Para isso podemos usar o método Series.map(dict) que transforma cada elemento usando-o como chave e retornando o valor no dicionário. Construímos um mapeamento entre produto e S/N, conforme o produto seja ou não vegano.

» veg = {'leite':'N', 'manteiga':'N', 'laranja':'S', 'arroz':'S'}
» # dfComprar['produto'] é uma Series e
» dfComprar['produto'].map(lambda x: vegano[x])
↳ 0    N
  1    N
  2    S
  3    S

» # inserindo esse serie em uma nova coluna do df
» dfComprar['vegano'] = dfComprar['produto'].map(veg)
» dfComprar
↳      produto   quantos   vegano
  0      leite        15        N
  1   manteiga        40        N
  2    laranja        50        S
  3      arroz        30        S

» # o mesmo resultado seria obtido com a função lambda 
» dfComprar['vegano']=dfComprar['produto'].map(lambda x: vegano[x])

Compartimentação e discretização

Compartimentação e discretização, (Binning e Discretization ) é o processo de particionamento de dados em faixas especificadas. Os compartimentos (faixas ou bins) são representados por variáveis categóricas, que são variáveis que podem assumir apenas um número discreto e limitado de valores, geralmente fixo. Elas estão associadas à propriedades qualitivas do sistema que se observa e podem satisfazer ou não algum critério de ordenamento.

Por ex., suponha que temos um estudo de qualquer natureza centrada sobre indivíduos onde o sexo e a faixa etária são relevantes para as conclusões que se procura obter. O sexo dos indivíduos (digamos que divididos em F = feminino, M = masculino, O = outros) não pode ser ordenado. Mas as faixas etárias são ordenáveis. Dividimos a população estudada em faixas ou bins. Sabendo que todos os participantes são maiores de idade e nenhum tem mais de 98 anos de idade usamos as faixas separadas pelas idades: 18, 34, 50, 66, 82, 98 anos.

» faixas = [18, 34, 50, 66, 82, 98]                            # definição dos intervalos de idade
» idades = [25, 18, 59, 39, 68, 26, 73, 63, 56, 84]            # idade dos indivíduos no estudo
» categorias = pd.cut(idades, faixas)
» categorias
↳ [(18.0, 34.0], NaN, (50.0, 66.0], (34.0, 50.0], (66.0, 82.0], (18.0, 34.0],
   (66.0, 82.0], (50.0, 66.0], (50.0, 66.0], (82.0, 98.0]]
  Categories (5, interval[int64]): [(18, 34] < (34, 50] < (50, 66] < (66, 82] < (82, 98]]

» # o objeto categorias é do tipo Categorical
» type(categorias)
↳ pandas.core.arrays.categorical.Categorical

» categorias.categories
↳ IntervalIndex([(18, 34], (34, 50], (50, 66], (66, 82], (82, 98]],
                closed='right', dtype='interval[int64]')

# O método pd.value_counts(categorias) fornece uma contagem para cada valor existente:
» pd.value_counts(categorias)
↳ (50, 66]    3
  (18, 34]    2
  (66, 82]    2
  (34, 50]    1
  (82, 98]    1

» # as colunas são formadas por
» pd.value_counts(categorias).index[0],  pd.value_counts(categorias)[0]
↳ (Interval(50, 66, closed='right'), 3)

» nomes_faixas = ['garoto','adulto','semi-novo','vô','matusa']
» categorias = pd.cut(idades, faixas, labels=nomes_faixas)
» categorias
↳  ['garoto', NaN, 'semi-novo', 'adulto', 'vô', 'garoto', 'vô', 'semi-novo', 'semi-novo', 'matusa']
   Categories (5, object): ['garoto' < 'adulto' < 'semi-novo' < 'vô' < 'matusa']
» pd.value_counts(categorias)
↳ semi-novo    3
  garoto       2
  vô           2
  adulto       1
  matusa       1

» # podemos transformar esse objeto em um dataframe
» dfCont = pd.DataFrame(pd.value_counts(categorias))

» # reordenar índices
» dfConf = dfCont.reindex(['garoto', 'adulto', 'semi-novo', 'vô', 'matusa'])
» dfConf
↳             0
  garoto      2
  adulto      1
  semi-novo   3
  vô          2
  matusa      1

As faixas numéricas são estabelecidas em intervalos do tipo (a, b] < (b, c] … representando intervalos abertos no limite inferior e fechados no superior. Isso significa que a não está no primeiro intervalo, mas b está. Para alterar esse comportamento usamos o parâmetro pandas.cut(...,right=False).

Podemos informar em quantas faixas queremos dividir os dados, ao invés de passar explicitamente essas faixas. Nesse caso o método pandas.cut(dados, n, precision=p) calculará n intervalos iguais baseados nos valores máximos e mínimos dos dados. precision=p determina a precisão decimal das faixas.

» # array com 20 numeros aleatórios    
» dados = np.random.rand(20)*10

» dados.min(), dados.max()         # valores mínimo e máximo
↳ (1.0012658194039414, 9.799331139583924)

» # 3 faixas (bins)
» picado = pd.cut(dados, 3, precision=2)
» pd.value_counts(picado)
↳ (6.87, 9.8]     10
  (0.99, 3.93]     7
  (3.93, 6.87]     3

Para distribuir dados em faixas baseadas em quantis usamos o método pandas.qcut(dados, n), onde n é o número de partes na partição. Intervalos de quantis customizados podem ser conseguidos passando-se uma lista em pandas.qcut(dados, lista).

» data = np.random.randn(1000)          # 1000 números aleatórios
» categorias = pd.qcut(data, 4)         # distribui em quartis
» pd.value_counts(categorias)
↳ (-3.0309999999999997, -0.683]    250
  (-0.683, 0.0106]                 250
  (0.0106, 0.702]                  250
  (0.702, 3.196]                   250

» # intervalos de quantis customizados
» pd.value_counts(pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]))
↳ (-1.223, 0.0106]                 400
  (0.0106, 1.301]                  400
  (-3.0309999999999997, -1.223]    100
  (1.301, 3.196]                   100

Permutações aleatórias

Permutações entre as linhas (ou colunas) de um dataframe são obtidas com dataframe.take(arr), onde arr é um array com a ordem dos índices desejada. Se essa ordem for “sorteada” o dataframe fica com linhas em ordem “aleatoria”. Para reordenar colunas usamos axis=1. dataframe.sample(n) seleciona n linhas do dataframe, sem repetições (n < dataframe.shape[0]) e dataframe.sample(n, replace=True) retorna n linhas do dataframe que podem ser repetidas (como em um sorteio com reposição dos elementos sorteados).

» # dataframe de teste    
» df = pd.DataFrame(np.arange(16).reshape((4, 4)))
» df
↳      0    1    2    3
  0    0    1    2    3
  1    4    5    6    7
  2    8    9   10   11
  3   12   13   14   15

» sorteio = np.random.permutation(4)   # permutação aleatória de 0, 1, 2 e 3
» sorteio
↳ array([3, 2, 0, 1])

» df.take(sorteio)                     # dataframe na ordem de linhas sorteadas
↳       0     1     2     3
  3    12    13    14    15
  2     8     9    10    11
  0     0     1     2     3
  1     4     5     6     7

» df.take(sorteio, axis=1)             # dataframe na ordem de colunas sorteadas
↳       3     2     0     1
  0     3     2     0     1
  1     7     6     4     5
  2    11    10     8     9
  3    15    14    12    13

» df.sample(n=2)                       # 2 linhas selecionadas aleatoriamente
↳       0     1     2     3
  1     4     5     6     7
  3    12    13    14    15

» df.sample(n=2, axis=1)               # 2 colunas selecionadas aleatoriamente
↳      3    1
  0    3    1
  1    7    5
  2   11    9
  3   15   13

» df.sample(n=4, replace=True)        # 4 linhas selecionadas aleatoriamente, com reposição
↳      0    1    2    3
  0    0    1    2    3
  0    0    1    2    3
  1    4    5    6    7
  0    0    1    2    3

O mesmo dataframe obtido com df.take(sorteio) poderia ser conseguido com df.iloc[sorteio].

Indicador de computação, variáveis fictícias

Na estatística, econometria e aprendizado de máquina uma variável fictícia (variável dummy ) é uma representação de um efeito categórigo assumindo apenas os valores 0 ou 1 para indicar presença ou ausência de alguma forma de caracterização. Elas podem ser consideradas como representações numéricas de aspectos qualitativos. Um exemplo simples seria a representação da classificação de uma conta bancária como poupança (0) ou conta corrente (1).

Uma variável categórica pode ser transformada em uma matriz dummy ou de indicadores. Se uma series (uma coluna de um dataframe) possui p valores distintos podemos obter um dataframe com o mesmo número de colunas, cada uma contendo apenas 0 ou 1. Para isso usamos o método pandas.get_dummies(Series) que retorna um dataframe marcando as posições onde cada um dos p valores ocorrem. Um prefixo pode ser acrescentado aos nomes das colunas com pandas.get_dummies(Series, prefix='p').

Por ex., em uma pesquisa foi marcado, para cada indivíduo participante, o campo sexo = F (feminino), M (masculino), O (outros).

» df = pd.DataFrame({'individuo': ['Fulano', 'Beltrano', 'Cicrano', 'Deltrano', 'Cruciano', 'Marciano'],
                     'sexo': ['H','H','H','F','O','F']})
» df
↳      individuo   sexo
  0       Fulano      H
  1     Beltrano      H
  2      Cicrano      H
  3     Deltrano      F
  4     Cruciano      O
  5     Marciano      F

» # categorizando a coluna 'sexo'
» pd.get_dummies(df['sexo'])
↳      F    H    O
  0    0    1    0
  1    0    1    0
  2    0    1    0
  3    1    0    0
  4    0    0    1
  5    1    0    0

» # inserindo um prefixo (no nome das colunas)
» pd.get_dummies(df['sexo'], prefix='sexo').head(2)
↳    sexo_F  sexo_H  sexo_O
   0      0       1       0
   1      0       1       0

Muitas vezes os dados devem ser manipulados e preparados para uma devida categorização. Suponha que temos uma lista de autores, cada um associado a um ou mais gêneros literários separados por |. Queremos uma listagem de autores versus gêneros, marcando em qual gênero cada um escreve.

» # importamos de qualquer fonte o seguinte dataframe:
» dfAutores
↳       autor                genero
  0   Antonio          poesia|conto
  1      José               romance
  2     Marco      ficção|biografia
  3     Pedro          poesia|conto

» # cada autor está associado a um ou mais gêneros
» genero = dfAutores.genero
» autores = dfAutores.autor
» # as duas séries têm o mesmo comprimento (len(autores) = len(generos) = 4, 4

Criamos uma lista vazia e a preenchemos com todos os gêneros, quebrando os campos em |. Depois usamos pandas.unique(lista) para conseguir um array com os gêneros, sem repetições, como em um conjunto (set).

» lista = []
» for t in genero:
»     lista.extend(t.split('|'))
» unicos = pd.unique(lista)
» unicos
↳ array(['poesia', 'conto', 'romance', 'ficção', 'biografia'], dtype=object)

Em seguida criamos um dataframe de zeros com os autores nas colunas e gêneros nas linhas.

» dfZero = pd.DataFrame(np.zeros((len(unicos),len(autores))), index=unicos, columns=autores).astype(int)
» dfZero          # estado inicial de dfZero
↳ autor    Antonio    José    Marco    Pedro
  poesia         0       0        0        0
  conto          0       0        0        0
  romance        0       0        0        0
  ficção         0       0        0        0
  biografia      0       0        0        0

# preenchemos esse dataframe
» for i in range(len(unicos)):
»     for k in range(len(genero)):
»         if unicos[i] in genero[k]:
»             dfZero.iloc[i,k] = 1
            
» dfZero    # estado final de dfZero
↳ autor    Antonio    José    Marco    Pedro
  poesia         1       0        0        1
  conto          1       0        0        1
  romance        0       1        0        0
  ficção         0       0        1        0
  biografia      0       0        1        0

O duplo loop sobre a lista de gêneros únicos, unicos, e a lista original de gêneros genero faz a verificação se um dos generos está em genero1|genero2…. Por exemplo, na linha 3, coluna 2 temos:

» unicos[3], genero[2],  unicos[3] in genero[2]
↳ ('ficção', 'ficção|biografia', True)

Se o resultado é verdadeiro o dataframe terá o campo correspondente trocado para 1. Os demais permanecem com o valor 0. O dataframe final é o resultado desejado.

Tratamento de campos de texto

Operações com strings são também vetorializadas no pandas. No ex. usamos os códigos telefones dos países: 55-Brasil, 47-Noruega, 52-México. Construímos duas séries e as concatenamos em um dataframe, df = pd.concat([serie, srPais], axis=1).

» lista = ['055-11-12345678', '047-21-87654321', '055-11-13579135', '052-78-45665412']
» serie = pd.Series(lista)
» serie
↳ 0    055-11-12345678
  1    047-21-87654321
  2    055-11-13579135
  3    052-78-45665412

» # booleano, linhas que contém '-11-'
» serie.str.contains('-11-')
↳ 0     True
  1    False
  2     True
  3    False

» # linhas que contém '-11-'
» serie[serie.str.contains('-11-')]
↳ 0    055-11-12345678
  2    055-11-13579135

» # lista com os códigos dos países
» codigos = [x.split('-')[0] for x in lista]
» codigos
↳ ['055', '047', '055', '052']

» # dicionário para conversão código ⇒ país
» pais = {'055':'Brasil', '047':'Noruega', '052':'México'}
» srPais = pd.Series([pais[x] for x in codigos])      # veja comentário †
» srPais
↳ 0     Brasil
  1    Noruega
  2     Brasil
  3     México

» # juntamos as duas series em um dataframe
» df = pd.concat([serie, srPais], axis=1)
» df = df.rename(columns = {0:'telefone', 1:'pais'})

» df
↳             telefone       pais
  0    055-11-12345678     Brasil
  1    047-21-87654321    Noruega
  2    055-11-13579135     Brasil
  3    052-78-45665412     México

» # nome do país começado com 'No'
» df[df['pais'].str.startswith('No')]
↳             telefone       pais
  1    047-21-87654321    Noruega

» # acrescenta campo com 3 primeiras letras do nome
» df['abreviado'] = df['pais'].str[:3]
» df
↳             telefone      pais    abreviado
  0    055-11-12345678    Brasil          Bra
  1    047-21-87654321   Noruega          Nor
  2    055-11-13579135    Brasil          Bra
  3    052-78-45665412    México          Méx

(): A linha srPais = pd.Series([pais[x] for x in codigos]) (uma compreensão de lista) percorre os valores em codigos e os usa como chaves no dicionário pais, retornando seus valores.

🔺Início do artigo

Bibliografia

  • McKinney, Wes: Python for Data Analysis, Data Wrangling with Pandas, NumPy,and IPython
    O’Reilly Media, 2018.

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

Series: Resumo


Pandas Series

Atributos

Atributo Descrição
at[n] Acesso ao valor na posição n
attrs Retorna ditionario de atributos globais da series
axes Retorna lista de labels do eixo das linhas
dtype Retorna o tipo (dtype) dos objetos armazenados
flags Lista as propriedades do objeto
hasnans Informa se existem NaNs
iat[n] Acesso ao valor na posição n inteiro
iloc[n] Acesso ao valor na posição n inteiro
index Retorna lista de índices
index[n] Retorna índice na n-ésima posição
is_monotonic Booleano: True se valores crescem de forma monotônica
is_monotonic_decreasing Booleano: True se valores decrescem de forma monotônica
is_unique Booleano: True se valores na series são únicos
loc Acessa linhas e colunas por labels em array booleano
name O nome da Series
nbytes Número de bytes nos dados armazenados
shape Retorna uma tuple com forma (dimensões) dos dados
size Número de elementos nos dados
values Retorna series como ndarray

Métodos

Método (sobre série s, outra s2) Descrição
s.abs() Retorna s com valor absoluto, e/e
s.add(s2) Soma s com s2, e/e
s.add_prefix('prefixo') Adiciona prefixo aos labels com string ‘prefixo’
s.add_suffix('sufixo') Adiciona sufixo aos labels com string ‘sufixo’
s.agg([func, axis]) Agrega usando func sobre o eixo especificado
s.align(s2) Alinha 2 objetos em seus eixos usando método especificado
s.all([axis, bool_only, skipna, level]) Booleano: se todos os elementos são True
s.any([axis, bool_only, skipna, level]) Booleano: se algum elemento é True
s.append(to_append[, ignore_index, …]) Concatena 2 ou mais Series
s.apply(func[, convert_dtype, args]) Aplica func sobre os valores de s, e/e
s.argmax([axis, skipna]) Posição (índice inteiro) do valor mais alto de s
s.argmin([axis, skipna]) Posição (índice inteiro) do menor valor de s
s.argsort([axis, kind, order]) Índices inteiros que ordenam valores da s
s.asfreq(freq) Converte TimeSeries para frequência especificada.
s.asof(where[, subset]) Último elemento antes da ocorrência de NaNs após ‘where’
s.astype(dtype[, copy, errors]) Transforma (cast) para dtype
s.at_time(time[, asof, axis]) Seleciona valores em determinada hora (ex., 9:30AM)
s.backfill([axis, inplace, limit, downcast]) Aliás para DataFrame.fillna() usando method=’bfill’
s.between(min, max) Booleana satisfazendo min <= s <= max, e/e
s.between_time(inicio, fim) Seleciona valores com tempo entre inicio e fim
s.bfill([axis, inplace, limit, downcast]) Alias para DataFrame.fillna() usando method=’bfill’
s.clip([min, max, axis, inplace]) Inclui apenas valores no intervalo
s.combine(s2, func[, fill_value]) Combina a s com s2 ou escalar, usando func
s.compare(s2[, align_axis, keep_shape, …]) Compara s com s2 exibindo differenças
s.copy([deep]) Cópia do objeto s, índices e valores
s.corr(s2) Correlação de s com s2, excluindo NaNs
s.count([level]) Número de observações na s, excluindo NaN/nulls
s.cov(s2[, min_periods, ddof]) Covariância da s, excluindo NaN/nulls
s.cummax([axis, skipna]) Máximo cumulativo
s.cummin([axis, skipna]) Mínimo cumulativo
s.cumprod([axis, skipna]) Produto cumulativo
s.cumsum([axis, skipna]) Soma cumulativa
s.describe([percentiles, include, exclude, …]) Gera descrição estatística
s.div(s2) Divisão (float) de s por s2, e/e
s.divmod(s2) Divisão inteira e módulo de s por s2, e/e
s.dot(s2) Produto interno entre a s e s2
s.drop([labels]) Retorna s com labels removidos
s.drop_duplicates([keep, inplace]) Remove elementos duplicados de s
s.dropna() Remove valores faltantes de s
s.duplicated([keep]) Exibe valores duplicados na s
s.eq(s2) Boleano, igualdade entre s e s2, e/e
s.equals(s2) Booleano: True se s contém os mesmos elementos que s2
s.ewm([com, span, halflife, alpha, …]) Calcula exponencial com peso
s.explode([ignore_index]) Transforma cada elemento de um objeto tipo lista em uma linha
s.fillna([value, method, axis, inplace, …]) Substitui valores NA/NaN usando método especificado
s.first(offset) Seleciona período inicial de uma série temporal usando offset.
s.first_valid_index() Índice do primeiro valor não NA/null
s.floordiv(s2) Divisão inteira da s por s2, e/e
s.ge(s2) Booleana: maior ou igual entre s e s2, e/e
s.get(key) Retorna item correspondente à key
s.groupby([by, axis, level, as_index, sort, …]) Agrupa a s
s.gt(s2[, level, fill_value, axis]) Booleana: se s é maior que s2, e/e
s.head([n]) Retorna os n primeiros valores
s.hist() Plota histograma da s usando matplotlib.
s.idxmax([axis, skipna]) Label do item de maior valor
s.idxmin([axis, skipna]) Label do item de menor valor
s.interpolate([method, axis, limit, inplace, …]) Preenche valores NaN usando metodo de interpolação
s.isin(valores) Booleano: se elementos da s estão contidos em valores
s.isna() Booleano: se existem valores ausentes
s.isnull() Booleano: se existem valores nulos
s.item() Primeiro elemento dos dados como escalar do Python
s.items() Iteração (lazy) sobre a tupla (index, value)
s.iteritems() Iteração (lazy) sobre a tupla (index, value)
s.keys() Alias de index
s.kurt([axis, skipna, level, numeric_only]) Kurtosis imparcial
s.kurtosis([axis, skipna, level, numeric_only]) Idem
s.last(offset) Seleciona período final de uma série temporal usando offset
s.last_valid_index() Índice do último valor não NA/null
s.le(s2) Booleana: se s é menor ou igual a s2, e/e
s.lt(s2[, level, fill_value, axis]) Booleana: se s é menor que s2, e/e
s.mad([axis, skipna, level]) Desvio médio absoluto dos valores da s
s.mask(cond[, s2, inplace, axis, level, …]) Substitui valores sob condição dada
s.max([axis, skipna, level, numeric_only]) Valor máximo
s.mean([axis, skipna, level, numeric_only]) Média dos valores
s.median([axis, skipna, level, numeric_only]) Mediana dos valores
s.memory_usage([index, deep]) Memória usada pela s
s.min([axis, skipna, level, numeric_only]) Menor dos valores da s
s.mod(s2[, level, fill_value, axis]) Módulo de s por s2, e/e
s.mode([dropna]) Moda da s
s.mul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.multiply(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.ne(s2[, level, fill_value, axis]) Booleana: se s é diferente de s2, e/e
s.nlargest([n, keep]) Retorna os n maiores elementos
s.notna() Booleana: se existem valores não faltantes ou nulos
s.notnull() Idem
s.nsmallest([n, keep]) Retorna os n menores elementos
s.nunique([dropna]) Retorna quantos elementos únicos existem na s
s.pad([axis, inplace, limit, downcast]) O mesmo que DataFrame.fillna() usando method=’ffill’
s.plot O mesmo que pandas.plotting._core.PlotAccessor
s.pop(i) Remove s[i] de s e retorna s[i]
s.pow(s2) Exponential de s por s2, e/e
s.prod([axis, skipna, level, numeric_only, …]) Produto dos elemetos da s
s.product([axis, skipna, level, numeric_only, …]) Idem
s.quantile([q, interpolation]) Valor no quantil dado
s.ravel([order]) Retorna dados como um ndarray
s.rdiv(s2[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.rdivmod(s2) Divisão inteira e módulo de s por s2, e/e
s.reindex([index]) Ajusta a s ao novo índice
s.reindex_like(s2[, method, copy, limit, …]) Série com índices em acordo com s2
s.rename([index, axis, copy, inplace, level, …]) Altera o nome (labels) dos índices
s.reorder_levels(order) Reajusta níveis de índices usando order
s.repeat(repeats[, axis]) Repete elementos da s
s.replace([to_replace, value, inplace, limit, …]) Substitui valores em to_replace por value
s.reset_index([level, drop, name, inplace]) Reinicializa índices
s.rfloordiv(s2[, level, fill_value, axis]) Divisão inteira de s por s2, e/e
s.rmod(s2[, level, fill_value, axis]) Modulo da divisão da s por s2, e/e
s.rmul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.round([n]) Arredonda valores da s para n casas decimais.
s.rpow(s2[, level, fill_value, axis]) Exponential de s por s2, e/e
s.rsub(s2[, level, fill_value, axis]) Subtração da s por s2, e/e
s.rtruediv(serie[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.sample([n, frac, replace, weights, …]) Amostra randomizada de items da s
s.searchsorted(value[, side, sorter]) Índices onde elementos devem ser inseridos para manter ordem
s.sem([axis, skipna, level, ddof, numeric_only]) Erro padrão imparcial da média
s.skew([axis, skipna, level, numeric_only]) Inclinação imparcial
s.sort_index([axis, level, ascending, …]) Reorganiza s usando os índices
s.sort_values([axis, ascending, inplace, …]) Reorganiza s usando seus valores
s.std([axis, skipna, level, ddof, numeric_only]) Desvio padrão da amostra
s.str Usa funções de string sobre s (se string). Ex. s.str.split(“-“)
s.sub(s2) Subtração de s por s2, e/e
s.subtract(serie) Idem
s.sum([axis, skipna, level, numeric_only, …]) Soma dos valores da s
s.tail([n]) Últimos n elementos
s.to_clipboard([excel, sep]) Copia o object para o clipboard do sistema
s.to_csv([path_or_buf, sep, na_rep, …]) Grava a s como arquivo csv
s.to_dict() Converte s para dict {label ⟶ value}
s.to_excel(excel_writer[, sheet_name, na_rep, …]) Grava s como uma planilha Excel
s.to_frame([name]) Converte s em DataFrame
s.to_hdf(path_or_buf, key[, mode, complevel, …]) Grava s em arquivo HDF5 usando HDFStore
s.to_json([path_or_buf, orient, date_format, …]) Converte s em string JSON
s.to_latex([buf, columns, col_space, header, …]) Renderiza objeto para LaTeX
s.to_markdown([buf, mode, index, storage_options]) Escreve a s em formato Markdown (leia)
s.to_numpy([dtype, copy, na_value]) Converte s em NumPy ndarray
s.to_pickle(path[, compression, protocol, …]) Grava objeto serializado em arquivo Pickle
s.to_sql(name, con[, schema, if_exists, …]) Grava elementos em forma de um database SQL
s.to_string([buf, na_rep, float_format, …]) Constroi uma representação string da s
s.tolist() Retorna uma lista dos valores
s.to_list() idem
s.transform(func[, axis]) Executa func sobre elementos de s
s.truediv(s2) Divisão (float) de s por s2, e/e
s.truncate([before, after, axis, copy]) Trunca a s antes e após índices dados
s.unique() Retorna os valores da s, sem repetições
s.update(s2) Modifica s usando valores de s2, usando índices iguais
s.value_counts([normalize, sort, ascending, …]) Retorna s com a contagem de valores únicos
s.var([axis, skipna, level, ddof, numeric_only]) Variância imparcial dos valores da s
s.view([dtype]) Cria uma nova “view” da s
s.where(cond[, serie, inplace, axis, level, …]) Substitui valores se a condição cond = True
🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.