Análise de dados no Python: Diárias e passagens gaúchas

Olá! Este é um exercício em análise e exploração de dados em Python. Aqui, vamos aplicar as funcionalidades do pacote pandas em um arquivo que contém o histório de concessões de diárias e passagens a pessoas físicas no estado do Rio Grande do Sul, em 2017. Os dados contém informações sobre órgãos responsáveis, origens e destinos de viagens, pessoas beneficiadas e valores pagos em R$ correntes de 2017.

Vamos começar carregando os pacotes necessários:

%matplotlib inline
import pandas as pd
import numpy as np
import seaborn as sns
sns.set()

Agora, vamos fazer o download dos dados do Portal da Transparência do RS. Os dados vêm em formato *.csv, com o separador ponto-e-vírgula:

#!curl http://antigo.transparencia.rs.gov.br/ARQUIVOS/Diarias-RS-2017.zip
#!unzip Diarias-RS-2017.zip
df = pd.read_csv('Diarias-RS-2017.csv', sep=';', encoding='windows-1252')

Agora, vamos ver como os dados foram importados:

df.head()
Exercicio Mes Poder Cod_Setor_Gov Setor Cod_orgao Orgao Cod_UO UO Cod_Credor ... DataInicio DataFim Quantidade QuantidadeMeia Destino Tipo Motivo Valor Origem Empenho
0 2017 1 PODER EXECUTIVO 19 DIRETA 20 SECRETARIA DA SAUDE 2095 FUNDO ESTADUAL DE SAUDE 6998 ... 14/12/2016 14/12/2016 0 1 BUTIA DIARIA COMUM VENCID-CIVIL INSPECOES/AUDITORIAS 61,50 CHARQUEADAS 17000146930
1 2017 1 PODER EXECUTIVO 19 DIRETA 20 SECRETARIA DA SAUDE 2095 FUNDO ESTADUAL DE SAUDE 6998 ... 07/12/2016 07/12/2016 0 1 CAMAQUA DIARIA COMUM VENCID-CIVIL INSPECOES/AUDITORIAS 61,50 CHARQUEADAS 17000146996
2 2017 1 PODER EXECUTIVO 19 DIRETA 20 SECRETARIA DA SAUDE 2095 FUNDO ESTADUAL DE SAUDE 6998 ... 28/12/2016 28/12/2016 0 1 SAO JERONIMO DIARIA COMUM VENCID-CIVIL INSPECOES/AUDITORIAS 61,50 CHARQUEADAS 17000157622
3 2017 1 PODER EXECUTIVO 19 DIRETA 20 SECRETARIA DA SAUDE 2095 FUNDO ESTADUAL DE SAUDE 6998 ... 18/01/2017 18/01/2017 0 1 BARRA DO RIBEIRO DIARIA COMUM VENCID-CIVIL REUNIOES TECNICAS 61,50 CHARQUEADAS 17000303577
4 2017 1 PODER EXECUTIVO 19 DIRETA 15 SECRETARIA DA AGRICULTURA, PECUARIA E IRRIGACAO 1501 GABINETE E ORGAOS CENTRAIS 8745 ... 25/10/2016 25/10/2016 0 1 SANTA MARIA DIARIA COMUM VENCID-CIVIL REUNIOES ADMINISTRATIVAS 61,50 JARI 16006041272

5 rows × 21 columns

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 268874 entries, 0 to 268873
Data columns (total 21 columns):
Exercicio         268874 non-null int64
Mes               268874 non-null int64
Poder             268874 non-null object
Cod_Setor_Gov     268874 non-null int64
Setor             268874 non-null object
Cod_orgao         268874 non-null int64
Orgao             268874 non-null object
Cod_UO            268874 non-null int64
UO                268874 non-null object
Cod_Credor        268874 non-null int64
Favorecido        268874 non-null object
DataInicio        262676 non-null object
DataFim           262676 non-null object
Quantidade        268874 non-null int64
QuantidadeMeia    268874 non-null int64
Destino           268530 non-null object
Tipo              268874 non-null object
Motivo            268874 non-null object
Valor             268874 non-null object
Origem            268874 non-null object
Empenho           268874 non-null int64
dtypes: int64(9), object(12)
memory usage: 43.1+ MB

A coluna com os valores dos empenhos foram interpretadas como texto, por causa da vírgula como separador decimal. Vamos corrigir:

df['Valor'] = df.Valor.str.replace(pat=',', repl='.')
df['Valor'] = df.Valor.astype(float)

Agora, vamos ocultar o nome completo dos beneficiados, para fins didáticos:

df['Favorecido2'] = df.Favorecido.str.split()
df['Favorecido2'] = df.Favorecido2.apply(lambda x: x[-1])

A partir daqui já podemos fazer algumas análises, com o uso da função groupby do pacote pandas. Com ela é possível obter estatísticas agregadas por grupos. Vamos descobrir quais servidores receberam os maiores valores em diárias e passagens:

df.groupby(['Cod_Credor','Favorecido2'])['Valor'].sum().nlargest(10)
Cod_Credor  Favorecido2
21594481    PAESE          60088.33
21544697    CAYE           51664.02
21570051    SILVA          51336.00
10161040    RAMOS          51110.52
34346732    GARCIA         49799.23
18567100    MAINARDI       48719.62
6051006     PEIXOTO        48598.50
23170433    BORBA          48479.53
33839395    SILVA          47125.32
40092020    GOMES          45246.32
Name: Valor, dtype: float64

Ou então, quais os maiores fluxos entre unidades orçamentárias e órgãos que concedem os benefícios:

df.groupby(['UO','Orgao'])['Valor'].sum().nlargest(10).round()
UO                                            Orgao                                          
BRIGADA MILITAR                               SECRETARIA DA SEGURANCA PUBLICA                    28326579.0
DEPARTAMENTO AUTONOMO DE ESTRADAS DE RODAGEM  DEPARTAMENTO AUTONOMO DE ESTRADAS DE RODAGEM       13128503.0
SUPERINTENDENCIA DOS SERVICOS PENITENCIARIOS  SECRETARIA DA SEGURANCA PUBLICA                     7612260.0
CORPO DE BOMBEIROS MILITAR                    SECRETARIA DA SEGURANCA PUBLICA                     4765460.0
DEPARTAMENTO ESTADUAL DE TRANSITO             DEPARTAMENTO ESTADUAL DE TRANSITO                   3887284.0
POLICIA CIVIL                                 SECRETARIA DA SEGURANCA PUBLICA                     3766606.0
ASSEMBLEIA LEGISLATIVA                        ASSEMBLEIA LEGISLATIVA                              3193683.0
GABINETE E ORGAOS CENTRAIS                    SECRETARIA DA AGRICULTURA, PECUARIA E IRRIGACAO     3156240.0
PROCURADORIA-GERAL DE JUSTICA                 MINISTERIO PUBLICO                                  3103149.0
GABINETE E ORGAOS CENTRAIS                    SECRETARIA DA SEGURANCA PUBLICA                     3085936.0
Name: Valor, dtype: float64

Quais são as viagens mais frequentes?

df.groupby(['Origem','Destino'])['Quantidade'].sum().nlargest(20)
Origem            Destino       
PORTO ALEGRE      ALEGRETE          67886
                  ANTONIO PRADO     60853
                  AGUDO             45462
                  TRAMANDAI         21107
                  PORTO ALEGRE      18483
PASSO FUNDO       PORTO ALEGRE      12799
PORTO ALEGRE      CAPAO DA CANOA    12470
SANTA MARIA       PORTO ALEGRE      11074
PORTO ALEGRE      TORRES             7322
                  CIDREIRA           6514
                  OSORIO             5372
                  CHARQUEADAS        4953
SANTO ANGELO      PORTO ALEGRE       4769
PELOTAS           PORTO ALEGRE       4201
SANTA MARIA       CAPAO DA CANOA     3617
PORTO ALEGRE      CAXIAS DO SUL      3492
CACHOEIRA DO SUL  PORTO ALEGRE       3478
PELOTAS           RIO GRANDE         3272
PORTO ALEGRE      PASSO FUNDO        2959
SANTA MARIA       NOVO HAMBURGO      2881
Name: Quantidade, dtype: int64

Quais viagens representam os maiores valores gastos em 2017? Elas coincidem com os valores mostrados acima?

df.groupby(['Origem','Destino'])['Valor'].sum().nlargest(20)
Origem            Destino       
PORTO ALEGRE      TRAMANDAI         2721824.31
PASSO FUNDO       PORTO ALEGRE      1985408.88
SANTA MARIA       PORTO ALEGRE      1796298.20
PORTO ALEGRE      CAPAO DA CANOA    1627620.17
                  ALEGRETE          1252773.67
                  ANTONIO PRADO     1139786.58
                  TORRES             975725.28
N/D               N/D                946698.35
PORTO ALEGRE      DF                 925746.10
                  CIDREIRA           819431.60
                  AGUDO              801317.16
                  OSORIO             793824.58
SANTO ANGELO      PORTO ALEGRE       746511.73
PELOTAS           PORTO ALEGRE       720271.00
PORTO ALEGRE      CAXIAS DO SUL      635554.39
                  EXTERIOR           593282.20
                  SP                 579742.97
CACHOEIRA DO SUL  PORTO ALEGRE       554328.30
CHARQUEADAS       PORTO ALEGRE       550244.71
PORTO ALEGRE      CHARQUEADAS        548256.21
Name: Valor, dtype: float64

Cada diária/passagem deve ter registrada um motivo associado:

df.groupby('Motivo')['Valor'].sum().nlargest(20).round()
Motivo
OPERACAO LITORAL             25177625.0
SERVICOS TECNICOS             9045394.0
REFORCOS A OUTRA OPM          8213390.0
OPERACAO CANARINHO            3538243.0
RESSARC SERVIDOR BPR DAER     3413129.0
PARTICIP.EM TREIN./CURSOS     2990233.0
SERV.CONS.PAV.RODOV.-DAER     2892201.0
FISCAL.DE SERV.CONCEDIDOS     2797213.0
TRANSPORTE DE PESSOAS         2056261.0
PARTICIPACAO-EVENTOS          1971834.0
REUNIOES ADMINISTRATIVAS      1929856.0
ADMINISTRATIVO MP             1711359.0
SERV.PATRULH.RODOVIARIO       1629358.0
SERVICOS GERAIS               1623286.0
A SERVICO DE DEPUTADO         1527813.0
DEPUT.EST. A SERV.MANDATO     1309648.0
REUNIOES TECNICAS             1261205.0
ESCOLTA PRESOS -REMOCAO       1013571.0
PARTICIP-SEMINARIOS/CONGR      970380.0
SERV.DE ADMINISTR.-DAER        913763.0
Name: Valor, dtype: float64

Aqui, cruzamos destinos e servidores associados:

df.groupby(['Destino','Favorecido2'])['Valor'].sum().nlargest(20).round()
Destino         Favorecido2
PORTO ALEGRE    SILVA          1163673.0
                SANTOS          551193.0
                MACHADO         408099.0
                OLIVEIRA        378335.0
TRAMANDAI       SILVA           357969.0
PORTO ALEGRE    PEREIRA         296562.0
                ROSA            256169.0
CAPAO DA CANOA  SILVA           253770.0
PORTO ALEGRE    LIMA            245712.0
                JUNIOR          227397.0
                SOUZA           225926.0
TORRES          SILVA           214180.0
PORTO ALEGRE    RODRIGUES       207616.0
TRAMANDAI       OLIVEIRA        193649.0
PORTO ALEGRE    ALMEIDA         175780.0
                MORAES          174702.0
                SILVEIRA        171049.0
TRAMANDAI       SANTOS          170904.0
PORTO ALEGRE    COSTA           159793.0
CIDREIRA        SILVA           151463.0
Name: Valor, dtype: float64

Agora, vejamos quais foram os maiores valores reservados a determinado servidor em 2017:

x = df.sort_values(by='Valor', ascending=False).loc[:10,['Orgao','Favorecido2','Destino','Motivo','Valor']]
x.head(10)
Orgao Favorecido2 Destino Motivo Valor
158461 SECRETARIA DE DESENVOLVIMENTO ECONOMICO, CIENC... FONTANA EXTERIOR MISSAO OFICIAL 14056.90
210104 ASSEMBLEIA LEGISLATIVA DZIEDRICKI PARIS - FRANCA. DIARIAS ESPECIAIS DE VIAGEM DEPUT.EST. A SERV.MANDATO 13800.24
610 ASSEMBLEIA LEGISLATIVA LOPES PARIS - FRANCA DIARIAS ESPECIAIS DE VIAGEM DEPUT.EST. A SERV.MANDATO 13555.60
26539 ASSEMBLEIA LEGISLATIVA SIMONI Basileia - SUICA DIARIAS ESPECIAIS DE VIAGEM DEPUT.EST. A SERV.MANDATO 13468.00
196248 SECRETARIA DO AMBIENTE E DESENVOLVIMENTO SUSTE... PELLINI EXTERIOR PARTICIPACAO-EVENTOS 13019.26
210715 SECRETARIA DO AMBIENTE E DESENVOLVIMENTO SUSTE... MOLLMANN EXTERIOR PARTICIPACAO-EVENTOS 13019.26
207645 SECRETARIA DO AMBIENTE E DESENVOLVIMENTO SUSTE... MEIRELLES EXTERIOR PARTICIPACAO-EVENTOS 13019.26
163526 SECRETARIA DE DESENVOLVIMENTO ECONOMICO, CIENC... SCHUNEMANN EXTERIOR MISSAO OFICIAL 12521.29
198315 ASSEMBLEIA LEGISLATIVA BECKER BONN - ALEMANHA. DIARIAS ESPECIAIS DE VIAGEM DEPUT.EST. A SERV.MANDATO 12370.24
196438 SECRETARIA DE DESENVOLVIMENTO ECONOMICO, CIENC... SCHAFER EXTERIOR PARTICIPACAO-EVENTOS 11593.73

A seguir, veremos quais servidores requisitaram o maior número de diárias/passagens:

x = df.groupby(['Cod_Credor','Favorecido2'])
ind = x.size().nlargest(10).reset_index()
ind
Cod_Credor Favorecido2 0
0 6137113 DENKVITTS 192
1 21180091 DIAS 190
2 20682816 LERMEN 188
3 21164762 RAMOS 188
4 21169330 RODRIGUES 188
5 21153450 MARCO 181
6 26210711 WITT 180
7 26079267 COUTO 179
8 21171416 IRENO 178
9 21187487 RAMOS 178

Mais de 180 dias! Lembrando que o calendário comercial tem 252 dias no ano. Podemos cruzar a base ind gerada acima com a base de dados original df para calcular a diária média paga para estes servidores:

x = ind.merge(df)
x.groupby(['Cod_Credor','Favorecido2']).mean()['Valor']
Cod_Credor  Favorecido2
6137113     DENKVITTS      143.500000
20682816    LERMEN          61.500000
21153450    MARCO           88.002597
21164762    RAMOS           61.500000
21169330    RODRIGUES       61.500000
21171416    IRENO           61.500000
21180091    DIAS            61.500000
21187487    RAMOS           61.500000
26079267    COUTO           80.963128
26210711    WITT            64.916389
Name: Valor, dtype: float64
x.groupby('Mes').mean()['Valor']
Mes
1     143.500000
2      84.204194
3      78.925000
4      75.089494
5      80.033904
6      71.261429
7      76.178333
8      70.804043
9      72.729108
10     75.749390
11     72.090350
12     72.155921
Name: Valor, dtype: float64

Vamos acompanhar a evolução dos gastos destes servidores, mês a mês:

y = pd.DataFrame(x.groupby(['Mes','Favorecido2'])['Valor'].sum())
y.unstack().plot(kind='line', subplots=False)
<matplotlib.axes._subplots.AxesSubplot at 0x21235266c50>

png

Até aqui exploramos os gastos em termos de valores. Vamos agora para a frequência das requisições de diárias! Os dados disponibilizados pelo Portal da Transparência informam as datas iniciais e finais associadas à cada empenho (nas variáveis DataInicio e DataFim) bem como a quantidade de diárias pagas (nas variáveis Quantidade e QuantidadeMeia). Meias diárias são pagas quando não há pernoite do servidor.

Com essas informações, é possível comparar a quantidade de diárias pagas com as datas registradas, bem como identificar o dia da semana.

df['DataInicio'] = pd.to_datetime(df.DataInicio, format='%d/%m/%Y')
df['DataFim'] = pd.to_datetime(df.DataFim, format='%d/%m/%Y')

df['QuantosDias'] = df['DataFim'] - df['DataInicio']
df['QuantosDias'] = df.QuantosDias.dt.days

Vamos ver as viagens transcorridas durante finais de semana. Para isso, criarei uma função que indica se há um sabado ou domingo num intervalo entre datas, retornando verdadeiro ou falso, e depois aplicaremos essa função nos nossos dados.

def sabdom(x):
    """Retorna True se há sábado ou domingo num intervalo entre datas"""
    a = pd.date_range(start=x['DataInicio'], end=x['DataFim'], freq='D').dayofweek # dias entre inicio e fim da diaria
    b = np.in1d(a, [5,6]).any() # 5 = sábado, 6 = domingo
    return b
df.apply(sabdom, axis=1)

Deu erro! Isto acontece pois temos alguns valores faltantes em DataInicio e DataFim. Vamos criar uma versão dessa base sem esses valores e tentar novamente:

df2 = df.dropna()
x   = df2.apply(sabdom, axis=1)
df2['SabDom'] = x
df2.SabDom.value_counts()/len(df2)
False    0.798103
True     0.201897
Name: SabDom, dtype: float64

Segundo o cálculo acima, em 20% dos casos, a diária transcorre sábados e domingos. Podemos analisar os motivos associados com a função pivot_table:

tab = df2.pivot_table('Quantidade', index='Motivo', columns='SabDom', aggfunc=np.count_nonzero, margins=True, fill_value=0)
tab.head()
SabDom False True All
Motivo
A SERV.COMIS.TECN.PERMAN. 81 42 123
A SERVICO DE DEPUTADO 863 847 1710
A SERVIÇO DA FASE 1 0 1
ACOMP.DE INVENT.DE B.MOV. 43 8 51
ACOMP.DE INVENT.DE ESTOQ. 0 0 0

Acima, temos a contagem de dos casos para cada motivo. Podemos criar uma coluna para analisar a frequência de casos positivos em relação ao total:

tab['Percent'] = tab[True]/tab['All']
tab.sort_values(by='Percent', ascending=False, inplace=True)
#tab.sort_values(by=['Percent'], axis=1, inplace=True)
tab.head(20)
SabDom False True All Percent
Motivo
ATEND.REPART.FISC./EXAT. 0 1 1 1.000000
CONTATOS COMUNIDADE 0 1 1 1.000000
FISCALIZACAO TAXA CDO 0 49 49 1.000000
OPERAC. TURISMO/FRONTEIRA 0 30 30 1.000000
RESSARC SERVIDOR BPR DAER 1 7590 7591 0.999868
OPERACAO LITORAL 221 10049 10270 0.978481
MUTIRAO CARTORARIO 6 265 271 0.977860
OPERACAO CANARINHO 52 2094 2146 0.975769
PERFURACAO DE POCOS 11 207 218 0.949541
SERV.PATRULH.RODOVIARIO 138 1272 1410 0.902128
REFORCOS A OUTRA OPM 913 4701 5614 0.837371
APOIO EQUIPES FISCALIZ 10 51 61 0.836066
TRANSF.P/NECESSID.SERVICO 9 21 30 0.700000
FISC. TRANSITO DE MERCAD. 6 14 20 0.700000
REF.A EVENTOS ESPEC. - BM 158 365 523 0.697897
INSPECAO SANITARIA/ANIMAL 7 13 20 0.650000
FISCALIZ/PATRULHA AMBIENT 3 5 8 0.625000
COMBATE AO ABIGEATO 23 35 58 0.603448
SUBSTITUICOES A FUNCION. 150 222 372 0.596774
ESTAGIOS 3 4 7 0.571429

Portanto, vemos que a maioria das diárias de fim de semana está de fato ligada à atividades de campo de servidores.

A tabela a seguir faz a tabulação cruzada entre número de diárias pagas e dias transcorridos de viagem, com o uso da função pivot_table:

tab = df.pivot_table(index='Quantidade', columns='QuantosDias', aggfunc=np.count_nonzero)
tab.head(10)
Cod_Credor ... Valor
QuantosDias 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 ... 61.0 69.0 77.0 92.0 96.0 154.0 168.0 273.0 304.0 340.0
Quantidade
0 153006.0 4295.0 1444.0 710.0 603.0 68.0 41.0 30.0 42.0 23.0 ... 1.0 1.0 1.0 1.0 2.0 NaN 2.0 2.0 1.0 2.0
1 725.0 39429.0 336.0 58.0 54.0 7.0 NaN 1.0 1.0 NaN ... NaN NaN NaN NaN NaN 3.0 NaN NaN 1.0 NaN
2 19.0 189.0 15437.0 194.0 122.0 8.0 NaN 3.0 1.0 2.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3 17.0 6.0 76.0 6487.0 177.0 7.0 2.0 2.0 4.0 2.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
4 7.0 1.0 NaN 608.0 6745.0 25.0 4.0 2.0 4.0 3.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
5 4.0 1.0 NaN NaN 50.0 2516.0 4.0 7.0 2.0 NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
6 4.0 NaN NaN NaN 1.0 372.0 507.0 1.0 16.0 10.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
7 NaN NaN NaN 1.0 NaN NaN 136.0 379.0 4.0 10.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
8 NaN NaN NaN NaN NaN NaN 9.0 82.0 140.0 1.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
9 NaN NaN NaN NaN NaN NaN NaN NaN 116.0 174.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

10 rows × 1134 columns

Na tabela acima podemos detectar alguns pontos fora da curva, como por exemplo, várias diárias pagas para atividades de um dia, e vice-versa.

tab = df.query('(Quantidade > 1 ) & (QuantosDias == 1)').loc[:,['Favorecido2','Quantidade','Valor']]
tab.head(20)
Favorecido2 Quantidade Valor
2698 ROLIM 2 245.98
2746 SENNA 2 245.98
6363 SILVA 2 245.98
6958 PEREIRA 2 245.98
6996 MELLO 2 245.98
7041 WEBER 2 245.98
7177 DIAS 2 245.98
7234 MOURA 2 245.98
7380 CARVALHO 2 245.98
8409 SOUTO 2 245.98
11116 POZZOBON 2 358.75
11125 SILVA 2 245.98
11173 MACHADO 2 245.98
14233 MARTINS 2 245.98
14274 GUIMARAES 2 245.98
14338 ROCHA 2 245.98
14341 RITTER 2 245.98
14520 SOUZA 2 245.98
14976 CAMPOS 2 245.98
15600 MACHADO 2 245.98

Vamos organizar por quantidade de diárias para entender melhor:

tab = tab.sort_values(by='Quantidade', ascending=False)
tab.head(20)
Favorecido2 Quantidade Valor
80827 MOIANO 29 -184.49
28505 FLORES 18 -738.00
257107 BRUNHAUSER 14 -122.99
168131 SILVA 14 -150.32
190620 SERPA 5 -123.00
27416 PERIN 4 526.12
222709 SOBROSA 3 -245.98
105213 PALADINI 3 2234.31
155972 SOUZA 3 -300.66
41910 MATTAR 3 698.75
24447 MACHADO 3 512.48
43525 LUZ 3 698.75
226281 FIORAVANSO 2 245.98
100498 RESCHKE 2 1560.00
101771 MEIRELLES 2 1560.00
102686 ROSSI 2 245.98
102695 VARGAS 2 245.98
246512 ALIARDI 2 245.98
111185 BIANCHI 2 540.00
112748 PIRES 2 123.00

Tudo certo! Aparentemente, a maioria dos registros trata de ressarcimentos.

Por enquanto, vamos parando por aqui! Entretanto, muito mais pode ser feito, e este exercício mostrou apenas algumas das capacidades de manipulação e cruzamento de dados com o Python, através do pacote pandas.