Git Product home page Git Product logo

crossfire's Introduction

hexagon crossfire

crossfire Python client

crossfire is a package created to give easier access to Fogo Cruzado's datasets, which is a digital collaborative platform of gun shooting occurrences in the metropolitan areas of Rio de Janeiro and Recife.

The package facilitates data extraction from Fogo Cruzado's open API.

Requirements

  • Python 3.9 or newer

Install

$ pip install crossfire

If you want to have access to the data as Pandas DataFrames:

$ pip install crossfire[df]

If you want to have access to the data as GeoPandas GeoDataFrames:

$ pip install crossfire[geodf]

Authentication

To have access to the API data, registration is required.

The email and password used in the registration can be configured as FOGOCRUZADO_EMAIL and FOGOCRUZADO_PASSWORD environment variables in a .env file:

FOGOCRUZADO_EMAIL=[email protected]
FOGOCRUZADO_PASSWORD=YOUR_PASSWORD

If you prefer not to use these environment variables, you can still use the credentials when instantiating a client.

Usage

List of states covered by the project

Get all states with at least one city covered by the Fogo Cruzado project:

from crossfire import states


states()

It is possible to get results in DataFrae:

states(format='df')

List of cities covered by the project

Get cities from a specific state covered by the Fogo Cruzado project.

from crossfire import cities


cities()

It is possible to get results in DataFrae:

cities(format='df')

Cities parameters

Name Required Description Type Default value Example
state_id ID of the state string None 'b112ffbe-17b3-4ad0-8f2a-2038745d1d14'
city_id ID of the city string None '88959ad9-b2f5-4a33-a8ec-ceff5a572ca5'
city_name Name of the city string None 'Rio de Janeiro'
format Format of the result string 'dict' 'dict', 'df' or 'geodf'

Listing occurrences

To get shooting occurrences from Fogo Cruzado dataset it is necessary to specify a state id in id_state parameter:

from crossfire import occurrences


occurrences('813ca36b-91e3-4a18-b408-60b27a1942ef')

It is possible to get results in DataFrae:

occurrences('813ca36b-91e3-4a18-b408-60b27a1942ef', format='df')

Or as GeoDataFrame:

occurrences('813ca36b-91e3-4a18-b408-60b27a1942ef', format='geodf')

Occurrences parameters

Name Required Description Type Default value Example
id_state ID of the state string None 'b112ffbe-17b3-4ad0-8f2a-2038745d1d14'
id_cities ID of the city string or list of strings None '88959ad9-b2f5-4a33-a8ec-ceff5a572ca5' or ['88959ad9-b2f5-4a33-a8ec-ceff5a572ca5', '9d7b569c-ec84-4908-96ab-3706ec3bfc57']
type_occurrence Type of occurrence string 'all' 'all', 'withVictim' or 'withoutVictim'
initial_date Initial date of the occurrences string, date or datetime None '2020-01-01', '2020/01/01', '20200101', datetime.datetime(2023, 1, 1) or datetime.date(2023, 1, 1)
final_date Final date of the occurrences string, date or datetime None '2020-01-01', '2020/01/01', '20200101', datetime.datetime(2023, 1, 1) or datetime.date(2023, 1, 1)
max_parallel_requests Maximum number of parallel requests to the API int 16 32
format Format of the result string 'dict' 'dict', 'df' or 'geodf'

Custom client

If not using the environment variables for authentication, it is recommended to use a custom client:

from crossfire import Client


client = Client(email="[email protected]", password="Rio&Pernambuco") # credentials are optional, the default are the environment variables
client.states()
client.cities()
client.occurrences('813ca36b-91e3-4a18-b408-60b27a1942ef')

Asynchronous use with asyncio

from crossfire import AsyncClient


client = AsyncClient()  # credentials are optional, the default are the environment variables
await client.states()
await client.cities()
await client.occurrences('813ca36b-91e3-4a18-b408-60b27a1942ef')

Credits

@FelipeSBarros is the creator of the Python package. This implementation was funded by CYTED project number 520RT0010 redGeoLIBERO.

Contributors

crossfire's People

Contributors

felipesbarros avatar cuducos avatar lgelape avatar sergiospagnuolo avatar

Stargazers

Davi Santos avatar William Rodrigues avatar Ana Paula Gomes avatar Renan Filipe Altrão avatar  avatar

Watchers

James Cloos avatar  avatar

Forkers

cuducos

crossfire's Issues

Criar API que não utiliza o `Client` diretamente

Permitir o uso como:

from crossfire import cities, occurrences, states

states()  # lista UFs
cities()  # lista cidades
occurences()  # lista ocorrências

Lembro que em algum momento você mencionou que gostaria de uma API em que a pessoa não precisasse instanciar uma classe. Ainda precisamos?

`format` Occurrences' parameter not working

Trying the crossfire package, I could see that the parameter format in Occurrences is useless.
Do not return df, gdf neigther an error when format doesn't exists:

from crossfire import occurrences
occurrences(id_state='813ca36b-91e3-4a18-b408-60b27a1942ef',
                id_cities='5bd3bfe5-4989-4bc3-a646-fe77a876fce0',
                format='fourty-two')



from crossfire import Client
client = Client()
occ = client.occurrences(id_state='813ca36b-91e3-4a18-b408-60b27a1942ef',
                id_cities='5bd3bfe5-4989-4bc3-a646-fe77a876fce0',
                format='fourty-two')

I could see that, in Occurrences, the client.get, has only url as parameter. Shouldn't format be passed also?

                occurrences, metadata = await self.client.get(url)

On the other hand, in cities, it is been used:

from crossfire import cities


cities(state_id='813ca36b-91e3-4a18-b408-60b27a1942ef', format='df')

@cuducos I can find a way to solve this. Just appreciate your consideration about ir.

Reestruturação pacote python

Tendo visto a tualização da API do Fogo Cruzado, decidi rever esse pacote.
Além da necessidade de atualizar endpoints, vi que sería possível fazer algumas melhorias...
Conversnado com o @cuducos, pensamos em:

  • Renomear módulos para simplificar o pacote (evitar redundância)
    #30
  • Reestruturar o código melhorando o processo de longing da API;
  • Separar o pacote R do Python;
    #62
  • Escrever documentação
    #27
  • Atualizar testes
    #9
  • Adicionar githubActions
    #16
    • Black
    • Ruff
    • Testes

wrong columns in nested_columns

During the construction of the function flatten I have added some columns names in the contant NESTED_COLUMNS.
Some of then are not nested columns as I believed according to the API documentation

Because of this, when running flatten function with no nested_columns parameter, an AttributeError ir returned.

So, the following values must be excluded from the constant

  • transports;
  • victims;
  • animalVictims

Also, some columns name should be added:

  • state;
  • region;
  • city;
  • neighborhood;
  • locality;

Excluir `extract_data_api`

Dado que:

  • extract_data_api não está documentado
  • em breve esses mesmos dados terão melhor API com métodos para cada recurso (occurences, states, cities…)
  • o corpo dessa função é praticamente o uma chamada ao novo Client
  • está planejado separar a API em Python da em R

Creio que podemos excluir essa função (e seus testes), removendo o último namespace duplicado (no estilo from crossfire import fogocruzado_something)

crossfire.occurrence sem parametros de initial e final_date

@cuducos acabei de ver que os parâmetros de initial_date e final_date` não foram "expostos" no crossfire.init.
com isso, aquela versão "simplificada" de uso:

from crossfire import occurrences


occurrences(id_state='813ca36b-91e3-4a18-b408-60b27a1942ef',
                id_cities='5bd3bfe5-4989-4bc3-a646-fe77a876fce0',
                initial_date='2018-04-01', 
                format='gdf')

Retorna TypeError; Pode deixar que eu resolvo isso... (com calma)

Remover código em R

O repositório atual está sendo usado para gestionar tanto o pacote Python como um pacote R (que, a princípio não está sendo atualizado).
Como estamos atualizando o pacote Python, apenas, deveriamos remover o relacionado a R. Caso, alguém queira atualizar o pacote R, basteria fazer um um branch do histórico e trabalhar num repositório específico.

Tornar `pandas` e `geopandas` opicionais

Quem for utilizar o pacote pode escolher uma das três forma de instalar:

$ pip install crossfire  # dados retornam, por padrão como um dicionário
$ pip install crossfire[pandas]  # dados retornam, por padrão como um pandas.DataFrame
$ pip install crossfire[geo]  # dados retornam, por padrão como um geopandas.DataFrame

Grande parte do código já está preparado para isso, mas o pyproject.toml ainda não — esse deve ser o foco dessa issue. A principal referência é a documentação do extras do Poetry.

`RuntimeError: This event loop is already running` when running crossfire not in concurrent way on Google Colab

@cuducos I was contacted by a user tying to use corssfire in colab and facing a RuntimeError.
He was trying to uso Client to fetch data and the RuntimeError was being returned.

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
[<ipython-input-6-5d1e3ea17d4a>](https://localhost:8080/#) in <cell line: 1>()
----> 1 c.states()

2 frames
[/usr/lib/python3.10/asyncio/base_events.py](https://localhost:8080/#) in _check_running(self)
    582     def _check_running(self):
    583         if self.is_running():
--> 584             raise RuntimeError('This event loop is already running')
    585         if events._get_running_loop() is not None:
    586             raise RuntimeError(

RuntimeError: This event loop is already running

The first thing I did was to try in my local machine. And I could confirm that the error is being generated in Google Colab (and probably on other cloud jupyter notebook). The only way I could fetch data on colab was using the Async Client.

Here is a colab showing the error with Client approach;
As I don't know much about the AsyncIO, could you help me on this taks?

Utilizar `asyncio` nas requisições para melhorar a performance

Quando fazemos uma requisição para obter ocorrências, se formos carregar todas, pode ser que demore 7 minutos. Isso pode ser acelerado se:

  • ao invés de fazer requisições sequenciais (requisão para página 1, requisção para página 2, etc)
  • fizermos requisições por mais páginas em paralelo (por exemplo, sabendo que temos dez páginas, já dispararmos a requisão para as páginas de 1 a 10 ao mesmo tempo)

Ficaria feliz em implementar seguindo mais ou menos essa linha:

  • Substituição da biblioteca requests pela httpx (que é melhor para requisções com asyncio)
    #51
  • No parse_response retornar todas os metadados ao invés de só o hasNextPage
    #48
  • Implementar na Occurences um tratamento para utilizar o pageCount (ou mesmo itemCount) para determinar a quantidade total de páginas
    metadata.page_count (resultado do #48) já resolve!
  • Criar um limite de requisições paralelas configurável
    #52
  • Refatorar Occurences para uso de um cliente async (ainda sem paralelismo)
    #53
  • Refatorar Occurences para realizar requisições em paralelo
    #64

Refatoração dos métodos `Client.state()` e `Client.cities()`

Com a alteração do parse_responde realizada no #35 , os métodos passam a retornar os dados e também a infomração sobre paginação. Tal implementação foi feita para podermos identificar, "internamente", quando os dados retornados de ocorrência estão completos ou possuem mais dados em outra páginas.

Como sugerido no #31 , para não obrigar o usuário final do pacote a, asignar o resultado de tais métodos a dois objetos difernetes, quando apenas um (o de dados) é o desejado, a proposta é criar uma camada mais de abstração.

Com essa alteração, os métodos que não precisam de paginação, retornarão apenas os dados.

class Client:
   # …
   def _states(self, *args, **kwargs):
       ... # método que existe hoje
       
   def states(self, *args, **kwargs):  # wrapper para fazer a API pública mais simples
       states, _ = self._states(*args, **kwargs)
       return states

Má formatação de pagina em Occurrences

Ao testar o pacote com o seguinte código da documentação:

from crossfire import AsyncClient

client = AsyncClient()  # credentials are optional, the default are the environment variables

await client.occurrences('813ca36b-91e3-4a18-b408-60b27a1942ef')

Tive o seguinte erro:

Loading pages:  32%|███▏      | 59/186 [00:20<00:12, 10.25page/s]Traceback (most recent call last):
  File "/opt/pycharm-community-2022.3.2/plugins/python-ce/helpers/pydev/pydevconsole.py", line 367, in runcode
    loop.run_until_complete(coro)
  File "/home/felipe/.pyenv/versions/3.10.2/lib/python3.10/asyncio/base_events.py", line 641, in run_until_complete
    return future.result()
  File "<input>", line 1, in <module>
  File "/home/felipe/repos/crossfire/crossfire/clients/__init__.py", line 128, in occurrences
    return await occurrences()
  File "/home/felipe/repos/crossfire/crossfire/clients/occurrences.py", line 114, in __call__
    pages = await gather(*requests)
  File "/home/felipe/repos/crossfire/crossfire/clients/occurrences.py", line 95, in page
    return await self.page(url)
  File "/home/felipe/repos/crossfire/crossfire/clients/occurrences.py", line 85, in page
    occurrences, metadata = await self.client.get(url)
  File "/home/felipe/repos/crossfire/crossfire/clients/__init__.py", line 92, in get
    response.raise_for_status()
  File "/home/felipe/.cache/pypoetry/virtualenvs/crossfire-WqfJa-6U-py3.10/lib/python3.10/site-packages/httpx/_models.py", line 758, in raise_for_status
    raise HTTPStatusError(message, request=request, response=self)
httpx.HTTPStatusError: Client error '400 Bad Request' for url 'https://api-service.fogocruzado.org.br/api/v2/occurrences?idState=813ca36b-91e3-4a18-b408-60b27a1942ef&typeOccurrence=all&page=https%3A%2F%2Fapi-service.fogocruzado.org.br%2Fapi%2Fv2%2Foccurrences%3FidState%3D813ca36b-91e3-4a18-b408-60b27a1942ef%26typeOccurrence%3Dall%26page%3D64'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400

Ao ver a mensagem de bad request contactei o Davi, desenvolvedor do Instituto Fogo Cruzado. Ele não testou a requisição pelo código mas usando a API, disse ter conseguido acessar todas as páginas sem problema. Sugerindo ser, possívelmente, um problema na formatação da pagina.
Olhando melhor o código, vi que a url da msg de erro, de fato, retorna https%3A%2F%2Fapi-service.fogocruzado.org.br%2Fapi%2Fv2%2Foccurrences%3FidState%3D813ca36b-91e3-4a18-b408-60b27a1942ef%26typeOccurrence%3Dall%26page%3D64 ao invés de retornar um valor inteira da página a ser requisitada.

httpx.HTTPStatusError: Client error '400 Bad Request' for url 'https://api-service.fogocruzado.org.br/api/v2/occurrences?idState=813ca36b-91e3-4a18-b408-60b27a1942ef&typeOccurrence=all&page=https%3A%2F%2Fapi-service.fogocruzado.org.br%2Fapi%2Fv2%2Foccurrences%3FidState%3D813ca36b-91e3-4a18-b408-60b27a1942ef%26typeOccurrence%3Dall%26page%3D64'

Split nested dict columns

I was reading about the occurrences endpoint and playing around with the package when I relized that some columns are returned as a Python dictionary with adicional data related with the violence case.

For example, alfter getting some data for Rio de Janeiro:

import pandas as pd
from crossfire import AsyncClient

client = AsyncClient()
states_dict, _ = await client.states(format="dict")
states_df, _ = await client.states(format="df")

cities_rj, _ = await client.cities(state_id=states_df.id.iloc[0], format="df")

occs = await client.occurrences(
    id_state=cities_rj.state.iloc[16].get("id"),
    id_cities=cities_rj.id.iloc[16],
    initial_date="2019-01-01",
    final_date="2019-01-01",
    format="df",
)

If I was interested on analyzing the main reasons related to the violence cases, I would need to "unpack" the contextInfo column and, then, the mainReason, transforming it to a Pandas.Series, joining them with the "original" DataFrame, dopping the flattened column and renaming it to mainReason to keep the meaning of the column:

# check columns from Occurrences request
>>> occs.columns
Index(['id', 'documentNumber', 'address', 'state', 'region', 'city',
       'neighborhood', 'subNeighborhood', 'locality', 'latitude', 'longitude',
       'date', 'policeAction', 'agentPresence', 'relatedRecord', 'contextInfo',
       'transports', 'victims', 'animalVictims'],
      dtype='object')

# flatten/split contextInfo into different columns and concatenate in the "original" DataFrame
>>> occs = pd.concat(
    [occs.drop(["contextInfo"], axis=1), occs["contextInfo"].apply(pd.Series)], axis=1
)

# checking columns names after flettening
>>> occs.columns
Index(['id', 'documentNumber', 'address', 'state', 'region', 'city',
       'neighborhood', 'subNeighborhood', 'locality', 'latitude', 'longitude',
       'date', 'policeAction', 'agentPresence', 'relatedRecord', 'transports',
       'victims', 'animalVictims', 'mainReason', 'complementaryReasons',
       'clippings', 'massacre', 'policeUnit'],
      dtype='object')

# Flatten/split the mainReason column to keep only the mainReason name and renaming it to mainReason
>>> occs = pd.concat(
    [occs.drop(["mainReason"], axis=1), occs["mainReason"].apply(pd.Series).name.rename('mainReason')], axis=1
)
>>> occs.columns
Index(['id', 'documentNumber', 'address', 'state', 'region', 'city',
       'neighborhood', 'subNeighborhood', 'locality', 'latitude', 'longitude',
       'date', 'policeAction', 'agentPresence', 'relatedRecord', 'transports',
       'victims', 'animalVictims', 'complementaryReasons', 'clippings',
       'massacre', 'policeUnit', 'mainReason'],
      dtype='object')

>>> occs.mainReason
0        Tiros a esmo
1    Não identificado
2    Não identificado
3    Não identificado
4    Não identificado
5    Não identificado
6    Não identificado
7       Ação policial
8    Não identificado
Name: mainReason, dtype: object

# counting occurrences grouping by mainReason
>>> occs.groupby("mainReason").size()
mainReason
Ação policial       1
Não identificado    7
Tiros a esmo        1
dtype: int64

So I thought that perhaps woud be interesting a method that do that "automagically".

Beyond
contextInfo, transport, victims and animalVictims could be candidates of this process.

So I thought something like occs.flatten() to have all solumns with dict transfomed in separeted columns.
Also, this potential method could be applied to a specific column, like occs.flatten("contextInfo")to flatten only the contextInfo column.
And also something like occs.flatten("contextInfo", flatten_all=True) to have all nested dicts inside contextInfo flatenned in differents columns.
What is your opinion, @cuducos ?

Excluir `extract_cities_api`

Dado que:

  • extract_cities_api não está documentado em nenhum lugar, nem é utilizado.
  • em breve esses mesmos dados terão melhor API com client.cities()
  • o corpo dessa função é o mesmo de extract_data_api
  • está planejado separar a API em Python da em R

Creio que podemos excluir essa função (e seus testes).

Wrong Occurrences lat and lon column names

When requesting Occurrences with format='geodf' the IncompatibleDataError is returned:

`IncompatibleDataError: Missing columns latitude_ocorrencia and longitude_ocorrencia. They are needed to create a GeoDataFrame.

Looking in the Occurrences endpoint API documentation I realized that the expectyed column name latitude_ocorrencia longitude_ocorrencia is not the one returned by the API. PR proposing fixing already created #97

Como rodar os testes?

Temos que documentar isso — e talvez implementar algumas melhorias.

Tentei com poetry run python -m unittest tests/test_crossfire.py mas:

  • isso é insustentável quando adicionarmos mais arquivos de teste
  • deu algusn erros pois faltavam variáveis de ambiente
  • criei variáveis de ambiente “simbólicas” (com valores aleatórios, mas com as chaves corretas) e deu erro, ou seja, parece que o teste está acessando a API de verdade (o que não é recomendado)

Remover diretorio `dist/`

Esse diretório é gerado automaticamente e não há razão para mantê-lo no repositório… ou estou esquecendo algum detalhe?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.