馃樇 Haseando y optimizando en pandas

馃悕 Python
馃棡 Data Engineering
馃搮 2023-02-20

En primer lugar he de darle las gracias a Mario Pinto por alentarme a escribir este post, recientemente nos hemos encontrado un c贸digo "mejorable" el cu谩l ya ten铆a una base de tests. El caso era que deb铆amos de remover de nuestros datos una serie de filas usando pandas en base a un conjunto de columnas, un filtro compuesto por as铆 decirlo. El caso era que el c贸digo que ten铆amos no estaba muy vectorizado para trabajar con datos. Veamos la tabla por la que hay que filtrar:

Tabla de filtrado

| ID  | year | category |
| --- | ---- | -------- |
| A1  | 1    | A        |
| B2  | 2    | B        |
| ... | ...  | ...      |

La idea aqu铆 era simplificar el c贸digo lo m谩ximo posible tanto el proceso como la complejidad del c贸digo por lo que se nos ocurri贸 generar una nueva columna con el valor hash siendo la suma de todos los elementos a filtrar. Nos dimos cuenta r谩pidamente de que pandas necesita usar su propia funci贸n de hashing, ya que usar la implementaci贸n de hash de python requer铆a un apply o un applymap.

Ejemplo con apply

df['hash'] = df.apply(lambda x: hash(x['ID'] + x['year'] + x['category']), axis = 1)

Ejemplo con sin apply

df['hash'] = pd.util.hash_array(
  df['ID'].to_numpy() + df['year'].to_numpy() + df['category'].to_numpy()
)

Usamos el .to_numpy() para convertir el valor de la pd.Series en un array que contiene los 3 valores [ID, year, category] para luego generar un n煤mero 煤nico en base a esos valores.

Si usas pandas.util.hash_pandas_object recuerda que hace un hash sobre el 铆ndice del objeto en cuesti贸n por lo que no nos sirve para comparar con un objeto con diferente 铆ndice en otro dataframe.

Ahora si ya tenemos en la tabla de filtrado la nueva columna.

Tabla de filtrado

| ID  | year | category | hash |
| --- | ---- | -------- | ---- |
| A1  | 1    | A        | 101  |
| B2  | 2    | B        | 110  |
| ... | ...  | ...      | ...  |

La funci贸n quedar铆a a帽adirla a los datos:

Tabla de datos

| ID  | year | category | hash | more_columns... |
| --- | ---- | -------- | ---- | --------------- |
| A1  | 1    | A        | 101  | ...             |
| B2  | 2    | B        | 110  | ...             |
| ... | ...  | ...      | ...  | ...             |

Y ahora solo quedar铆a filtrar para quedarnos con aquellos hashes que no aparezcan en la tabla de filtrado:

Filtrado de los datos

are_not_on_filter_table = (~data['hash'].isin(filter_table['hash']))
filtered = data[are_not_on_filter_table].drop(columns=['hash'])

Conclusi贸n

Este es un caso que se puede resolver de muchas maneras, en este caso hemos intentado resolverlo de una manera con pocas l铆neas de c贸digo, que no se usase apply en cuesti贸n de rendimiento va bastante bien y es simple de a帽adir o quitar columnas para filtrar. Si por ejemplo hubiesen muchos valores en la tabla de filtrado el .isin() tendr铆a que comprobar para todas las filas de data buscar por las N filas de la tabla de filtrado, probablemente haya soluciones mejores como merge.