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.
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)
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]
Si hacemos un pequeño recuento:
- Existen filas con comentarios que empiezan por el carácter '#'
- El separador es ';' en lugar del esperado ',' (recordemos que se trata de un archivo CSV o valores separados por comas (comma-separated values).
- 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)
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
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)
Tenemos ahora la siguiente lista, aunque no es demasiado autoexplicativa.
{'A Coruña': [2689], 'A Yun Pa': [15026], 'Aabenraa': [28671], 'Aachen': [2698], [...]
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"))
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
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)
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
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}"
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))
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="")
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)