DEV Community

Cover image for Pandas y pitones
Baltasar García Perez-Schofield
Baltasar García Perez-Schofield

Posted on • Edited on

Pandas y pitones

Indirectamente, a través de un vídeo de Youtube, me enteré de un concurso llamado 1brc, es decir, un billón de filas (one billion row count), un concurso sobre programar con Java la forma más rápida de procesar un archivo de datos CSV.

El concurso ya había terminado, pero me pareció simpático tratar de hacerlo con Python. Y la forma más sencilla sin duda es utilizar Pandas.

Para instalarlo, basta con un python -m pip install -U pandas en la línea de comandos.

Python y Pandas

Mediante Pandas, podemos incluso cargar directamente el archivo CSV de datos en el directorio data/ del repo, creando un DataFrame.

import pandas as pd
from datetime import datetime


url = "https://github.com/gunnarmorling/1brc/raw/main/data/weather_stations.csv"
t1 = datetime.now()

# Compile the data
df_temperatures_by_city = pd.read_csv(url)
print(df_temperatures_by_city)
Enter fullscreen mode Exit fullscreen mode

Si observamos la salida del programa, tenemos varios pequeños problemas...

      # Adapted from https://simplemaps.com/data/world-cities
0      # Licensed under Creative Commons Attribution ...     
1                                          Tokyo;35.6897     
2                                        Jakarta;-6.1750     
3                                          Delhi;28.6100     
4                                      Guangzhou;23.1300     
...                                                  ...     
44687                                      Numto;63.6667     
44688                                       Nord;81.7166     
44689                                Timmiarmiut;62.5333     
44690                                San Rafael;-16.7795     
44691                                    Nordvik;74.0165     

[44692 rows x 1 columns]
Enter fullscreen mode Exit fullscreen mode

Si hacemos un pequeño recuento:

  1. Existen filas con comentarios que empiezan por el carácter '#'
  2. El separador es ';' en lugar del esperado ',' (recordemos que se trata de un archivo CSV o valores separados por comas (comma-separated values).
  3. El número de filas no es un billón. Supongo que se trata solo de una muestra del archivo. Bueno, en realidad esto no me afecta, porque solo quiero hacer este pequeño experimento.

Si consultamos la documentación de Pandas para read_csv(), veremos que para solventar el cambio de delimitador tenemos el parámetro delimiter; para evitar los comentarios, podemos establecer '#' como indicador de comentario con comment='#'. Finalmente, leyendo la documentación encontraremos adecuado también evitar las líneas en blanco con skip_blank_lines=True.

Además, podemos indicarle que queremos utilizar ciertos nombres para las columnas, como city para la primera (de tipo cadena de caracteres), y temperatures (de tipo número real o float), para la segunda. Esto podemos indicarlo con el parámetro names para bautizar las columnas, y con dtype para indicar los tipos de dichas columnas. Así, nombraremos las columnas con names=("city", "temperature") por un lado, y dtype={"city": str, "temperature": float} por el otro.

df_temperatures_by_city = pd.read_csv(url,
                                sep=';',
                                names=("city", "temperature"),
                                dtype={"city": str, "temperature": float},
                                comment='#',
                                skip_blank_lines=True)
print(df_temperatures_by_city)
Enter fullscreen mode Exit fullscreen mode

La salida ahora tiene mucha mejor pinta.

              city  temperature
0            Tokyo      35.6897
1          Jakarta      -6.1750
2            Delhi      28.6100
3        Guangzhou      23.1300
4           Mumbai      19.0761
...            ...          ...
44686        Numto      63.6667
44687         Nord      81.7166
44688  Timmiarmiut      62.5333
44689   San Rafael     -16.7795
44690      Nordvik      74.0165
Enter fullscreen mode Exit fullscreen mode

Nos interesa agrupar estos datos por ciudades, de manera que tengamos todos sus datos de temperatura juntos. Para ello, podemos utilizar el método groupby("nombre_de_columna"), que alternativamente puede tomar una lista de columnas si quisiéramos agrupar los datos por varias de ellas.

# Compile the data
df_temperatures_by_city = pd.read_csv(url,
                                sep=';',
                                names=("city", "temperature"),
                                dtype={"city": str, "temperature": float},
                                comment='#',
                                skip_blank_lines=True).groupby("city")
print(df_temperatures_by_city.groups)
Enter fullscreen mode Exit fullscreen mode

Tenemos ahora la siguiente lista, aunque no es demasiado autoexplicativa.

{'A Coruña': [2689], 'A Yun Pa': [15026], 'Aabenraa': [28671], 'Aachen': [2698], [...]
Enter fullscreen mode Exit fullscreen mode

Si queremos mostrar los datos específicos de una ciudad, podemos hacerlo con el método get_group(city). En este caso, tiramos de patria y vamos a mostrar los datos de Coruña, y después los de París.

print(df_temperatures_by_city.get_group("A Coruña"))
print(df_temperatures_by_city.get_group("Paris"))
Enter fullscreen mode Exit fullscreen mode
          city  temperature
2689  A Coruña      43.3667
        city  temperature
36     Paris      48.8567
23091  Paris      33.6688
39778  Paris      36.2933
40147  Paris      38.2016
43521  Paris      39.6148
Enter fullscreen mode Exit fullscreen mode

Mmmm... sé lo que te estás preguntando. Claramente son grados farenheit, y convertidos a Celsius serían... Pero mejor que lo haga Pandas. Para convertir a grados Celsius desde Farenheit, debemos restar 32, multiplicar por 5, y dividir entre 9, es decir: (x - 32) * 5) / 9.

Lo único que tenemos que hacer es decirle a Pandas que tome un grupo (get_group("nombre")), y de ese grupo se centre en la columna de temperaturas (notación con corchetes: ["temperature"], o bien directamente como atributo; .temperature), y aplique la función de más arriba (esto se puede hacer con apply(), a la que podemos pasarle una lambda, como en apply(lambda x: (x - 32) * 5) / 9).

print(df_temperatures_by_city.get_group("A Coruña").temperature.apply(lambda x: (x - 32) * 5) / 9)
print(df_temperatures_by_city.get_group("Paris").temperature.apply(lambda x: (x - 32) * 5) / 9)
Enter fullscreen mode Exit fullscreen mode

Y así obtenemos la salida...

2689    6.314833
Name: temperature, dtype: float64

36       9.364833
23091    0.927111
39778    2.385167
40147    3.445333
43521    4.230444
Name: temperature, dtype: float64
Enter fullscreen mode Exit fullscreen mode

Nótese que solo estamos viendo la columna de temperatures, el número a la izquierda de cada valor es el número de fila.

Bueno, ya hemos jugado un tanto con Pandas. El problema pedía mostrar la temperatura mínima, la media y la máxima con un solo decimal para cada ciudad (ordenado alfabéticamente), en el formato {A Coruña=43.4/43.4/43.4, Paris=33.7/39.3/48.9,...}.

Por el momento está claro que debemos construir un diccionario que asocie cada ciudad con el mínimo de temperatura, la media y el máximo. Podemos ya guardarlos como texto con el formato pedido, para ahorrar pasos.

# Build a dictionary with the data
temperatures_by_city = {}
for city, df_group in df_temperatures_by_city:
    temperatures_by_city[city] = f"{df_group.temperature.min(): 3.1f}/" \
                                 f"{df_group.temperature.mean(): 3.1f}/" \
                                 f"{df_group.temperature.max(): 3.1f}"
Enter fullscreen mode Exit fullscreen mode

Así, por ejemplo, para la entrada A Coruña obtendremos 43.4/43.4/43.4 con temperatures_by_city["A Coruña"], mientras que para Paris obtendremos 33.7/39.3/48.9 mediante temperatures_by_city["Paris"].

Ya solo queda mostrar los datos en el formato pedido. Para ello, debemos ordenar las ciudades alfabéticamente, separarlas de sus datos con '=', y separar cada ciudad de la siguiente con una coma (',').

Podemos crear fácilmente una lista de ciudades ordenadas mediante la función sorted(): cities = sorted(temperatures_by_city.keys()).

De acuerdo, empecemos por formatear ciudades y temperaturas: "f"{city}={temperatures_by_city[city]}". De acuerdo, pero necesitamos recorrer todas las ciudades y crear una cadena de caracteres. Esto podemos hacerlo con comprensión de listas.

str.join(", ",
            (f"{city}={temperatures_by_city[city]}" for city in cities))
Enter fullscreen mode Exit fullscreen mode

En lugar de utilizar los corchetes para la comprensión de listas, empleamos un generador con los paréntesis para evitar precisamente crear una lista en memoria, ya que realmente solo necesitamos crear la cadena de caracteres, lo cuál ya es suficientemente memoria empleada.

Así, la parte final del código completo utilizando Pandas para calcular datos medios de temperaturas sería:

# Show
cities = sorted(temperatures_by_city.keys())

print("{",
      str.join(", ",
            (f"{city}={temperatures_by_city[city]}" for city in cities)),
      "}",
      sep="")

Enter fullscreen mode Exit fullscreen mode

Empleamos el parámetro sep="", que indica qué separador se utilizará entre los valores a mostrar, para conseguir que no se realice ninguna separación.

¡Pandas es muy útil! ¿Qué te ha parecido?

Top comments (0)