Pular para o conteúdo

    Capítulo 17, Intermediário

    Texto com o acessor str

    Nomes com espaços sobrando, siglas, acentos, o mesmo valor escrito de três jeitos: a maior parte da sujeira de uma planilha é texto. O acessor `.str` aplica as operações de texto à coluna inteira, de uma vez, e deixa os ausentes passarem.

    Separar e extrair

    O split com expand=True devolve colunas em vez de listas, e o extract usa uma expressão regular com grupos nomeados para tirar partes de um texto:

    intermediario/cap17_texto.pylinhas 10 a 21
    import pandas as pd
    
    pd.set_option("display.width", 170)
    pd.set_option("display.max_columns", 20)
    df = pd.read_csv("dados/funcionarios_100.csv").drop_duplicates("id_funcionario", keep="last")
    
    partes = df["nome"].str.split(" ", n=1, expand=True)
    partes.columns = ["primeiro_nome", "sobrenome"]
    print(partes.head(3))
    
    extraido = df["nome"].str.extract(r"(?P<primeiro>\S+)\s+(?P<ultimo>\S+)$")
    print(extraido.head(3))
    
    Saída
      primeiro_nome sobrenome
    0         Lucas     Nunes
    1          João     Rocha
    2        Olívia     Nunes
      primeiro ultimo
    0    Lucas  Nunes
    1     João  Rocha
    2   Olívia  Nunes
    

    O n=1 separa só no primeiro espaço, e o resto fica inteiro no sobrenome (importante para nomes compostos). Os grupos nomeados (?P<primeiro>) viram os nomes das colunas do resultado.

    Medir, buscar e testar

    intermediario/cap17_texto.pylinhas 26 a 29
    nomes = df["nome"]
    print(nomes.str.len().describe()[["min", "max"]].tolist())
    print(int(nomes.str.startswith("A").sum()), int(nomes.str.contains(r"^[A-C]", regex=True).sum()))
    print(int(nomes.str.count("a").sum()), nomes.str.upper().head(2).tolist())
    
    Saída
    [8.0, 16.0]
    4 11
    211 ['LUCAS NUNES', 'JOÃO ROCHA']
    

    O contains aceita uma expressão regular (regex=True por padrão), e o count conta as ocorrências de um padrão em cada texto. Em todos os casos, um valor ausente passa sem erro e continua ausente (ou False, nas respostas booleanas).

    Montar texto

    Somar colunas de texto com + funciona, mas qualquer ausente contamina o resultado: um texto somado a NaN é NaN. O str.cat deixa você escolher o que fazer com ele:

    intermediario/cap17_texto.pylinhas 34 a 37
    pequena = pd.DataFrame({"nome": ["Ana", "Bruno", "Carla"], "departamento": ["RH", None, "Marketing"]})
    print((pequena["nome"] + " (" + pequena["departamento"] + ")").tolist())
    print(pequena["nome"].str.cat(pequena["departamento"], sep=" - ", na_rep="sem depto").tolist())
    print(df["id_funcionario"].astype(str).str.zfill(6).head(2).tolist())
    
    Saída
    ['Ana (RH)', nan, 'Carla (Marketing)']
    ['Ana - RH', 'Bruno - sem depto', 'Carla - Marketing']
    ['001001', '001002']
    

    O zfill completa com zeros à esquerda, útil para códigos de largura fixa (o 1001 vira 001001).

    Achar o "mesmo" valor escrito de formas diferentes

    Dois cadastros, João Pereira e joao pereira, são a mesma pessoa, mas o Pandas não sabe. A solução é criar uma chave normalizada: sem acento, em minúsculas e com um espaço só entre as palavras. Duplicatas aparecem na chave, e não no texto original:

    intermediario/cap17_texto.pylinhas 42 a 52
    def chave(textos: pd.Series) -> pd.Series:
        return (
            textos.str.strip().str.lower()
            .str.normalize("NFKD").str.encode("ascii", errors="ignore").str.decode("ascii")
            .str.replace(r"\s+", " ", regex=True)
        )
    
    
    cadastro = pd.Series(["João Pereira", " joao  pereira ", "Maria Souza", "MARIA SOUZA"])
    print(chave(cadastro).tolist())
    print(int(cadastro.duplicated().sum()), int(chave(cadastro).duplicated().sum()))
    
    Saída
    ['joao pereira', 'joao pereira', 'maria souza', 'maria souza']
    0 2
    

    Sem normalizar, o duplicated() não acha nenhum (0). Com a chave, acha 2. É a técnica por trás de qualquer "deduplicar clientes", e é também o princípio das junções: duas tabelas só se encontram se as chaves forem escritas igual.

    Um aviso sobre expressões regulares

    Com o pyarrow instalado, as operações de texto do tipo str usam, quando podem, o motor de expressões regulares do Arrow, e não o módulo re do Python. Para os padrões comuns (\s, \S, \d, \w, grupos, âncoras) o resultado é o mesmo. Mas existem diferenças nas bordas: no capítulo 10, o escape \u0300 foi rejeitado pelo Arrow, enquanto o Python o aceita. A regra prática: prefira padrões simples e portáveis, e teste em um exemplo pequeno quando usar um recurso incomum.

    Texto e desempenho

    Operar sobre texto é mais caro que operar sobre números, e o tipo str do Pandas 3 ajuda. Em colunas com poucos valores distintos (departamento, cidade, canal), o tipo category do capítulo 19 economiza muita memória e acelera agrupamentos.

    Exercício 1

    Sobrenome e iniciais

    Escreva iniciais(serie), que receba uma Series de nomes completos e devolva as iniciais do primeiro e do último nome, em maiúsculas (por exemplo, Ana Souza vira AS). Teste com três nomes.