<jupyter_start><jupyter_text>Letalidade policial em São Paulo**Autores:** <NAME> e <NAME>--- INTRODUÇÃOElaborado para a oficina _Python para jornalistas_, evento do _2º Domingo de dados_ da Abraji, o presente trabalho busca emular uma análise exploratória feita com Python, cujos resultados poderiam subsidiar (ou descartar) uma pauta imaginada pela redação.Para esta análise, tomamos a temática "letalidade policial no estado de São Paulo". Destrincharemos dados públicos para entender:1. se houve evolução no número de mortes por intervenção da policia militar no decorrer dos anos;2. em quais cidades do estado essa evolução se apresenta de maneira mais clara;3. qual é o perfil das vítimas fatais da PM.Considerando o tempo dedicado à oficina (2 horas), algumas licenças foram tomadas pelos autores. São elas:1. a análise compreenderá os primeiros semestres de 2018, 2019 e 2020 -- o ideal seria trabalhar com um período mais abrangente;2. a base utilizada foi previamente formatada, seguindo a metodologia sugerida pela Secretaria de Segurança Pública de São Paulo -- no trabalho real, esta tarefa caberia ao jornalista de dados;3. a base compreende apenas óbitos decorrentes de intervenção de policial militar em serviço -- para uma reportagem, o ideal seria ter também os casos ocorridos com policiais militares fora de serviço.Duas bases reais foram utilizadas neste trabalho:1. Morte decorrente de intervenção policial, SSP-SP, [link](http://www.ssp.sp.gov.br/transparenciassp/Consulta.aspx)2. População, Fundação Seade, [link](http://www.imp.seade.gov.br/frontend//tabelas)  CONFIGURAÇÃO E CARREGAMENTO<jupyter_code>import pandas as pd
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)

import matplotlib.pyplot as plt
%matplotlib inline<jupyter_output><empty_output><jupyter_text>> `import` é o comando usado para carregar uma biblioteca. Aqui carregamos duas: Pandas (para ler as bases) e Matplotlib (para gerar gráficos).>> Mas para não ter de repetir os nomes das bibliotecas sempre que elas forem utilizadas, damos apelidos com `as`. Então, `import pandas as pd` indica que, sempre que chamarmos `pd`, estaremos nos referindo a `pandas`. Já `import matplotlib.pyplot as plt` indica que, sempre que `plt` for usado, estaremos fazendo referência ao módulo `pyplot` de `matplotlib`.>> Em seguinda, configuramos Pandas para mostrar todas as colunas e linhas quando forem criadas as tabelas (aqui chamadas de _dataframes_ ou simplesmente _df_). Por padrão, Pandas mostra uma quantidade limitada de linhas e colunas nos dataframes, mas queremos ver todas. Isso foi feito com `pd.set_option('display.max_columns', None)` e `pd.set_option('display.max_rows', None)`.>> Também usamos um comando para mostrar gráficos no próprio notebook, e não fora dele. Isso é feito configurando a biblioteca Matplotlib, com `%matplotlib inline`.<jupyter_code>obitos = pd.read_csv('../data/obitos.csv')

populacao = pd.read_csv('../data/populacao.csv')<jupyter_output><empty_output><jupyter_text>> `pandas.read_csv('caminho_do_arquivo')` é o comando usado para ler um arquivo `.csv` e transformá-lo num formato "padrão" para trabalhar com Pandas: o dataframe.>> Como atribuímos uma variável a isso (no caso do arquivo de mortes, a variável é `obitos`; no de população, `populacao`), cada vez que chamarmos a variável, estaremos nos referindo ao arquivo.>> Observação: Repare que usamos `pd.read_csv` em vez de `pandas.read_csv`. Isso porque, no `import`, demos a `pandas` o "apelido" `pd`.  CHECAGEM DOS DADOS<jupyter_code>obitos.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 1089 entries, 0 to 1088
Data columns (total 36 columns):
 #   Column                   Non-Null Count  Dtype  
---  ------                   --------------  -----  
 0   ANO_BO                   1089 non-null   int64  
 1   NUM_BO                   1089 non-null   int64  
 2   NUMERO_BOLETIM           1089 non-null   object 
 3   BO_INICIADO              1089 non-null   object 
 4   BO_EMITIDO               1089 non-null   object 
 5   DATAOCORRENCIA           1089 non-null   object 
 6   HORAOCORRENCIA           1064 non-null   object 
 7   PERIDOOCORRENCIA         1089 non-null   object 
 8   DATACOMUNICACAO          1089 non-null   object 
 9   DATAELABORACAO           1089 non-null   object 
 10  BO_AUTORIA               1089 non-null   object 
 11  FLAGRANTE                1089 non-null   object 
 12  LOGRADOURO               898 non-null    object 
 13  NUMERO                   1089 non-null   int64  
 14  BAIRRO                  [...]<jupyter_text>Agora que carregamos os dados dos dois arquivos, precisamos ver como eles estão, saber quais os tipos (texto? número inteiro? número decimal? etc.) de cada coluna, saber os nomes das colunas...Acima, fizemos isso com o dataframe `obitos`.> `df.info()` traz informações sobre o dataframe (que, no nosso caso, chama `obitos`).>> Vemos, por exemplo, que essa base tem 1089 registros; que há seis colunas com `float` ou `int` (números) e trinta com `object` (texto); que há colunas com registro de ano, nome da pessoa, cor, cidade etc.>> Vemos também que algumas colunas têm valores nulos -- ou seja, o total de registros menos a quantidade `non_null`. Isso pode ser um problema em algumas análises.<jupyter_code>obitos.iloc[0]<jupyter_output><empty_output><jupyter_text>> `df.iloc[n]` mostra o registro contido na linha `n`.>> No comando acima, observamos o registro que está na primeira linha -- `0` -- do dataframe chamado `obitos`. Por isso usamos `obitos.iloc[0]`.<jupyter_code>obitos.head(5)<jupyter_output><empty_output><jupyter_text>> `df.head(n)` mostra os `n` registros do começo do dataframe. >> Ou seja, `obitos.head(5)` retorna as cinco primeiras linhas do dataframe `obitos`.<jupyter_code>populacao.info()
populacao.iloc[0]
populacao.head(5)<jupyter_output><empty_output><jupyter_text>Observados os dataframes `obitos` e `populacao`, passamos à análise.  ANÁLISE Antes, vamos pontuar algo que observamos: cada linha de `obito` é uma pessoa morta por um policial militar em serviço. Pela lógica, **a quantidade de linhas representa a quantidade de pessoas mortas**.Isso significa, por exemplo, que, agrupando os registros por ano e contando as linhas, teremos a quantidade de mortos no primeiro semestre de cada ano.Vamos ver onde isso nos leva...<jupyter_code>obitos.groupby('ANO_BO').size()<jupyter_output><empty_output><jupyter_text>> Usamos `obitos.groupby('ANO_BO')` porque queremos agrupar (`groupby()`) os dados do dataframe `obitos` pela variável `ANO_BO`.>> Seguindo a lógica, se quiséssemos agrupar, digamos, pela cor da vítima, usaríamos `obitos.groupby('CORCUTIS')`; pela cidade, `obitos.groupby('CIDADE')`, e assim por diante.> > Porém, além de `groupby()`, usamos a função `size()`. Ela retorna a quantidade de linhas -- e, no nosso caso, linhas representam mortos.>> Desta forma, `obitos.groupby('ANO_BO').size()`é um comando para agrupar (por `ANO_BO`) e contar quantas linhas (ou quantos mortos) há para cada `ANO_BO`.O primeiro semestre de 2020 é o que registra mais óbitos por intervenção policial, em comparação ao mesmo período de anos anteriores.Mas qual é o aumento de 2020 em relação a 2019, em porcentagem?<jupyter_code>obitos.groupby('ANO_BO').size().pct_change()<jupyter_output><empty_output><jupyter_text>> O comando `pct_change()` mostra a mudança percentual entre um valor e o seu antecessor.>> Neste caso, `pct_change()` compara a quantidade de registros (`size()`) de 2018 com nada (pois nada o antecede, daí o resultado `NaN`, vazio), 2019 com 2018 e 2020 com 2019Observamos que tivermos um aumento de 9,29% no número de mortes por policiais comparando os primeiros semestres de 2018 e de 2019.Esse aumento foi ainda maior entre os primeiros semestres de 2019 e de 2020: 27,86%.Além de comparar os números por anos, é possível comparar entre as cidades. Para isso temos o dataframe `populacao`, com dados da população de cada município paulista nos anos de 2018, 2019 e 2020.Precisamos, portanto, vincular os dois dataframes.<jupyter_code>df = pd.merge(
    obitos, populacao, 
    left_on=['CIDADE', 'ANO_BO'], 
    right_on=['municipio', 'ano']
)<jupyter_output><empty_output><jupyter_text>> `pd.merge()` é a função para vincular, juntar dois dataframes. Aqui, `obitos` e `populacao`.>> Mas essa junção não pode ser aleatória. É preciso que haja pelo menos um elo entre os dois dataframes -- caso contrário, na nossa análise, seria capaz de a população de São Paulo ser atribuída ao município de Borá, o que seria catastrófico.>> Olhando novamente os dois dataframes, notamos que há duas colunas comuns, dois elos, apesar de os nomes serem diferentes: cidade (que chama `CIDADE` em `obitos` e `municipio` em `populacao`) e ano (`ANO_BO` em `obitos` e `ano` em `populacao`).>> Precisamos, então, avisar `pd.merge()` quais são esses elos. Fazemos isso com `left_on` (ou seja, as colunas do primeiro dataframe) e `right_on` (variáveis do segundo).>> O resultado dessa operação de junção é salva na variável `df`.<jupyter_code>df.head(5)
df.info()
df.iloc[0]
df = df[['ANO_BO', 'CIDADE', 'IDADE', 'CORCUTIS', 'populacao']]<jupyter_output><empty_output><jupyter_text>> Constatamos que `df` terminou com duas variáveis para ano e duas para cidade. Não faz sentido manter colunas repetidas, como também não faz sentido manter colunas que não usaremos para as análises. Por isso restringimos `df` às colunas que queremos. Fazemos isso passando uma lista de colunas (`['ANO_BO', 'CIDADE', 'IDADE', 'CORCUTIS', 'populacao']`) ao recorte de `df` (`df[]`) que queremos.>> Temos, portanto, `df[['ANO_BO', 'CIDADE', 'IDADE', 'CORCUTIS', 'populacao']]`.>> Aproveitamos para sobrescrever `df`, que agora terá apenas as colunas da lista.<jupyter_code>df250 = df[df['populacao'] >= 250000]<jupyter_output><empty_output><jupyter_text>> Queremos trabalhar apenas com municípios cuja população seja igual ou superior a 250 mil habitantes -- em código, isso seria escrito assim: `df['populacao'] >= 250000`. > > Precisamos, então, "fatiar" nosso dataframe. Precisamos dizer que, dos registros de `df`, queremos apenas aqueles que atendam à condição. Ou seja, `df[df['populacao'] >= 250000]`.>> O dataframe resultante dessa operação será chamado de `df250`.<jupyter_code>df250.head(20)
df250_consolidado = df250.groupby(['CIDADE', 'ANO_BO']).size().reset_index()<jupyter_output><empty_output><jupyter_text>> Além de `groupby()` e `size()`, que já vimos anteriormente, o comando `reset_index()` "formata" os resultados em dataframe, enumerando novamente as linhas.<jupyter_code>df250_consolidado.head(20)
df250_consolidado.rename(columns={0: 'quantidade'}, inplace=True)<jupyter_output><empty_output><jupyter_text>> A contagem em `df_250_consolidado` retornou uma coluna chamada `0`, que é a quantidade de mortos para cada cidade e ano. Renomeamos a coluna com `rename(columns={nome antigo: novo nome})`, e usamos `inplace=True` para essa alteração ser no próprio dataframe, sem a necessidade de gerar novo dataframe.<jupyter_code>df250_consolidado.head(20)
graf1 = pd.pivot_table(
    df250_consolidado, 
    values='quantidade', 
    index='CIDADE', 
    columns='ANO_BO'
)<jupyter_output><empty_output><jupyter_text>> Fica um tanto complicado fazer comparações com o dataframe da forma como está, com nomes de cidade repetidos (um para cada ano). Também não é a forma ideal para criar gráficos com Matplotlib.>> Precisamos mudar disso:>> | CIDADE | ANO_BO | quantidade |> |---|---|---|> | Cidade A | 2018 | 1 |> | Cidade A | 2019 | 2 |> | Cidade A | 2020 | 3 |> | Cidade B | 2018 | 10 |> | Cidade B | 2019 | 20 |> | Cidade B | 2020 | 30 |>> Para algo como:>> | CIDADE | 2018 | 2019 | 2020 |> |---|---|---|---|> | Cidade A | 1 | 2 | 3 |> | Cidade B | 10 | 20 | 30 |>> Para isso, usamos `pivot_table()`, função que rotaciona os dados dentro do dataframe. >> Também precisamos indicar o dataframe (`df250_consolidado`), qual coluna contém os valores (`values='quantidade'`), qual coluna será o índice (`index='CIDADE'`) e em qual coluna ficarão os valores (`columns='ANO_BO'`).>> Tudo isso será armazenado na variável `graf1`.<jupyter_code>graf1<jupyter_output><empty_output><jupyter_text>> A visualização já melhorou: conseguimos ver com mais clareza que Campinas, por exemplo, teve aumento de 2019 para 2020; Bauru, queda...>> Mas os números ganharam casa decimal: em vez de `1`, temos `1.0`. Isso é normal quando rotacionamos uma tabela com valores nulos (`NaN`): números inteiros viram pontos flutuantes. Mas não há impacto no cálculo.<jupyter_code>graf1.plot(kind='bar', figsize=(15,7), grid=True, legend=True)<jupyter_output><empty_output><jupyter_text>Em gráfico, fica mais fácil visualizar, dentre os municípios com 250 mil habitantes ou mais, aqueles que tiveram aumento entre os períodos.> O comando `plot()` é uma função de Pandas que herda de Matplotlib funcionalidades para plotar um gráfico a partir de um dataframe -- no caso, `graf1`.>> Aqui, usamos `kind='bar'` para mostrar um gráfico de barras, `figsize=(15,7)` para definir o tamanho do gráfico em polegadas, `grid=True` para trazer a grade que acompanha os eixos X e Y, e `legend=True` para exibir a legenda.Porém a comparação entre essas cidades só é possível de maneira proporcional à população. Ou seja, precisamos calcular a taxa de mortos pela polícia em cada, digamos, 10 mil habitantes.<jupyter_code>df250_expandido = df250.groupby(['CIDADE', 'ANO_BO']).agg(
    populacao=('populacao', 'mean'), 
    quantidade=('ANO_BO', 'size')
).reset_index()<jupyter_output><empty_output><jupyter_text>> O dataframe `df250` tem todas as mortes nas cidades com 250 mil habitantes ou mais distribuídas em linhas. Também tem a população de cada um dos municípios em cada um dos anos.>> Ou seja, uma cidade com, digamos, 30 mortos aparecerá em 30 linhas, e a população da cidade naquele ano será repetida 30 vezes.>> Para calcularmos a quantidade de mortos no `groupby` usamos `size`, conforme visto anteriormente. E para calcularmos a população, uma vez que esse valor aparece repetidas vezes? Simples: usamos a média, `mean`.>> Para ilustrar: se uma cidade de 2 mil habitantes aparece em nove linhas, a informação de população (2000) aparecerá nove vezes. Se somarmos, teremos um valor errado, de 18 mil habitantes. Se tirarmos a média, teremos o valor verdadeiro: 2 mil.>> Portanto, ao criarmos o dataframe (`df250_expandido`) com contagem de óbitos e população, agrupamos o dataframe original (`df250`) por cidade e ano (`groupby(['CIDADE', 'ANO_BO'])`), e a ele agregamos (`agg()`) a média da população (`'populacao', 'mean'`, ao qual damos o nome de `populacao`) e a contagem de mortos -- ou a contagem de linhas a partir de uma coluna qualquer, como a de ano -- (`ANO_BO`, `size`, ao qual damos o nome de `quantidade`).> > Daí temos:>> ```> df250_expandido = df250.groupby(['CIDADE', 'ANO_BO']).agg(>     populacao=('populacao', 'mean'), >     quantidade=('ANO_BO', 'size')> )> ```>> O comando `reset_index()`, conforme visto acima, reorganiza o dataframe.<jupyter_code>df250_expandido.head(20)
df250_expandido['taxa'] = (df250_expandido['quantidade'] / df250_expandido['populacao']) * 10000<jupyter_output><empty_output><jupyter_text>> A taxa por 10 mil habitantes segue a seguinte lógica: $\frac{mortos}{habitantes} \times 10000$.>> Logo, para o nosso dataframe, seria `(df250_expandido['quantidade'] / df250_expandido['populacao']) * 10000`.> > Gravamos essa taxa numa coluna nova chamada `taxa`, dentro do dataframe `df250_expandido`.<jupyter_code>df250_expandido.head(20)
graf2 = pd.pivot_table(df250_expandido, values='taxa', index='CIDADE', columns='ANO_BO')

graf2
graf2.plot(kind='bar', figsize=(15,7), grid=True, legend=True)<jupyter_output><empty_output><jupyter_text>Tomando a taxa de mortes pela polícia a cada 10 mil habitantes, vemos que Guarujá, em 2020, é uma cidade que se destaca.> Acima temos o gráfico das taxas por ano e cidade. Seguimos a mesma lógica e os mesmos comandos do gráfico anterior.Contudo, essa comparação entre cidades não mostra, de maneira precisa, a evolução no tempo -- ou seja, se de um período a outro, quanto a taxa em determinada cidade aumentou ou caiu.Vamos comparar a taxa nos anos.<jupyter_code>graf3 = graf2.pct_change(axis='columns')

graf3
graf3.plot(kind='bar', figsize=(15,7), grid=True, legend=True)<jupyter_output><empty_output><jupyter_text>Com a variação percentual, observamos que, de 2019 a 2020, 18 das 28 cidades tiveram aumento na taxa de letalidade policial, com destaque para Praia Grande. Além disso, a cidade de Limeira, que não contabilizou mortes em 2019, passou a contabilizar em 2020 -- e não consta na variação percentual porque não tem como calcular aumento percentual quando o valor anterior é 0.> Aqui utilizamos funções já conhecidas. A única diferença foi em `pct_change`, que recebeu o argumento `axis='columns'` -- ou seja, calcular a diferença percentual entre colunas, não entre linhas.Agora que temos um retrato da letalidade da PM por município e ano, vamos perfilar quem são as vítimas, especificamente a média de idade e a cor.<jupyter_code>obitos.groupby('ANO_BO').agg({'IDADE': 'mean'})<jupyter_output><empty_output><jupyter_text>No geral, vemos que as vítimas, em 2020, não chegaram à maioridade. Mas será que existe diferença na média de idade por cor? Vamos pegar os dados de 2020 e ver...<jupyter_code>cor_idade = obitos[obitos['ANO_BO'] == 2020]

cor_idade.groupby('CORCUTIS').agg(
    quantidade=('CORCUTIS', 'size'), 
    idade_media=('IDADE', 'mean')
).reset_index()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.100204848071014, 'fraction_numerical': 0.027028564925458062, 'mean_word_length': 4.897651006711409, 'pattern_counts': {'":': 0, '<': 91, '<?xml version=': 0, '>': 206, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '22', 'dir': 'jupyter-structured-clean-dedup', 'id': '333606', 'license': 'MIT', 'n_tokens_mistral': 6569, 'n_tokens_neox': 6092, 'n_words': 2155, 'path': '20200913_abraji_analisededadoscompython/src/work/letalidade_policial.ipynb', 'repo_name': 'rodolfo-viana/eventos', 'size': '17591'}