Pandas proporciona estructures de dades tabulades per a gestionar grans volums de dades de forma eficient. Permet ordenar, agrupar i obtenir estadístiques fàcilment.

Introducció

Pandas no es farà servir més (legacy)

A la pràctica, les dades sovint s'emmagatzemen en forma de taula, per exemple, un full de càlcul, un fitxer CSV o una base de dades SQL. Imagina’t que necessites analitzar aquestes dades tabulars i obtenir-ne informació útil.

En primer lloc, has de carregar dades de diferents formats conservant la seva estructura tabular i probablement unir diverses taules. Aleshores, per realitzar l'anàlisi de dades real, voldrás accedir a diferents columnes, files i cel·les de les taules, calcular estadístiques generals, crear taules dinàmiques i fer gràfics.

Hi ha una eina a Python que combini totes aquestes funcionalitats? La resposta és sí!

En aquesta activitat treballarem amb pandas, una biblioteca molt potent de codi obert per a la manipulació i anàlisi de dades. Aprendràs a instal·lar la biblioteca i et faràs una idea de la seva funcionalitat principal.

Avantatges de pandas

L’objetiu de pandas és convertir-se en "l'eina d'anàlisi/manipulació de dades de codi obert més potent i flexible disponible en qualsevol idioma".

El nom pandas deriva de "panel data", un terme que s'utilitza en estadístiques i econometria per referir-se a conjunts de dades que contenen observacions de les mateixes persones durant diversos períodes de temps.

Aquesta biblioteca és útil si treballes amb dades tabulars, com ara dades emmagatzemades en fulls de càlcul o bases de dades.

Amb l'ajuda de pandas, pots realitzar fàcilment els passos de processament de dades més típics. En particular, et permet carregar i desar dades de manera molt fàcil, ja que admet la integració immediata amb molts formats tabulars d'ús habitual, com ara .csv, .xlsx, així com bases de dades SQL.

Mirem què més podem obtenir dels pandes!

  • La fusió i unió intuïtives de conjunts de dades permeten combinar fàcilment dades de diferents fonts, mentre que les eines de remodelació flexibles ajuden a construir resums de dades estadístiques.

  • Els valors que falten a les dades es representen com NaN i es poden manejar fàcilment, per exemple, es poden substituir per algun valor, utilitzant la funcionalitat integrada.

  • bon suport per a sèries temporals i ofereix una àmplia funcionalitat per treballar amb dates i hores indexades.

  • Si instal·leu matplotlib, una biblioteca de traçat de Python, podeu utilitzar el pandas funcionalitat de traçat integrada per fer una trama bàsica a partir de les vostres dades per entendre-les millor.

  • Es pot integrar amb altres biblioteques per a l'aprenentatge automàtic, com ara sklearn o keras.

  • Finalment, pandas és un programari de codi obert, que el fa molt popular tant en els dominis acadèmics com comercials.

Fes una ullada a la documentació oficial de Pandas, per descobrir tot el potencial.


Entorn de treball

Crea un nou projecte amb Poetry:

$ poetry new plot --name app
$ cd plot
$ poetry add pandas numpy

Crea el fitxer app/pd-main.py i prova que pandas funciona:

import pandas as pd

dates = pd.date_range("20241030", periods=6, freq='D')
places = ['Sarria','Portomarín','Palas Do Rei','Arzúa','O Pedrouzo','Santiago']
s2 = pd.Series(data=places,index=dates)

Pots veure que s'ha creat una sèrie de noms indexada per dates en temps rècord.


Estructures de dades en pandes

Les dues estructures de dades de pandas són Series(1D) i DataFrame(2D).

Series és una matriu unidimensional que emmagatzema elements del mateix tipus de dades.

Cada element emmagatzemat en un objecte Series s'associa amb una etiqueta anomenada index. Per defecte, aquest índex és només una seqüència (0, 1, 2, ...) . Tanmateix, pots utilitzar qualsevol valor personalitzat.

Per exemple, quan s'analitzen sèries temporals, les marques de temps (timestamps) normalment s'estableixen com a índexs. Els índexs, així com l'alineació de dades automàtica i explícita basada en ells, són el nucli de pandas.

DataFrame, al seu torn, és una estructura de dades 2D que s'utilitza per representar dades tabulars amb columnes de tipus de dades potencialment diferents. Pots imaginar que un DataFrame és una taula, on cada columna de la taula és un objecte Series. En altres paraules, DataFrame és un contenidor per Series, mentre Series és un contenidor per a elements escalars.

En el exemple que es mostra a continuació, els tres objectes Series emmagatzemen noms, cognoms i edats dels estudiants, mentres que el DataFrame combina aquesta informació en una única taula.

A tenir en compte sobre els DataFrames

  • Cada fila del DataFrame també està associat a un índex.

  • Les taules que representen els DataFrame difereixen lleugerament dels fulls de càlcul amb l’objectiu d’optimitzar els càlculs. Les Series (que equivalen a les columnes del DataFrame) només poden emmagatzemar dades d'un mateix tipus per poder fer operacions molt més ràpid. Això és perquè un objecte Series utilitza una matriu NumPy; per tant, una Series de pandas té la mateixa limitació de tipus de dades que un array de Numpy

  • Tampoc és possible afegir-hi elements a un objecte Series, perquè per poder afegir un elements a un objecte Series s’ha de crear un nou objecte perquè l’array de numpy és de tamany fixe. També si eliminem un element d’un objecte Series es crea un nou objecte. En veritat, de poder es pot, però no s’ha de fer.

  • pandas està dissenyat per preservar la immutabilitat de les dades.

Preguntes tipus test.

1.- Anàlisi del preu de les accions de l'empresa Imagineu que esteu interessats a comprar accions d'una empresa determinada. Abans de prendre la decisió final, vol analitzar el comportament dels preus de les accions d'aquesta empresa al llarg del temps. Heu recopilat les dades següents: data i preu de les accions de l'empresa per a aquest dia. Voleu carregar aquestes dades i analitzar-les amb pandas. Quina estructura de dades de pandas es pot utilitzar per representar-la? Podria haver-hi més d'una resposta correcta. Consell: podeu establir la data com a índex

  1. DataFrame
  2. Series
  3. Array
  4. Aquestes dades no es poden representar en pandes, ja que conté observacions de diferents tipus de dades

2 .- Tipus Series Per què el dataframe Series de pandas només conté valors del mateix tipus?

  1. Les dades de diversos tipus (nombres enters, cadenes i flotants) es poden gestionar simultàniament dins del mateix Series
  2. L’objecte Series no imposa la condició de tenir dades del mateix tipus
  3. Perquè les dades de Series dades s'emmagatzemen en matrius NumPy i el requisit que els elements d'aquestes matrius siguin del mateix tipus de dades

3.- Predicció de preus mòbils Voleu crear un model per predir els preus dels telèfons mòbils segons les seves característiques tècniques. Ara tens dos fulls de càlcul amb característiques tècniques i preus. Quan hauríeu d'utilitzar els pandes?

  1. Combina dos conjunts de dades
  2. Avaluar el rendiment d'un model d'aprenentatge automàtic
  3. Realitzar operacions matemàtiques complicades en matrius
  4. Obteniu estadístiques bàsiques
  5. Corregiu les errades i ompliu tots els valors que falten

4.- Estudiants internacionals Tens accés a la base de dades d'un institut, que conté la següent informació sobre cada estudiant: número d'estudiant, nom, data de naixement, país d'origen i titulació.

Voleu carregar aquestes dades i analitzar-les amb pandas. Quin estructura de dades de pandes utilitzareu per representar-lo?

  1. DataFrame
  2. Aquestes dades no es poden representar en pandes, ja que conté observacions de diferents tipus de dades
  3. Array
  4. Series
  5. Múltiples Series separades

5 .- Què són els pandes ? Quines de les afirmacions següents són certes? Seleccioneu totes les opcions correctes.

  1. pandas proporciona funcionalitat per carregar, manipular i analitzar dades
  2. pandas forma part de la biblioteca estàndard de Python
  3. pandas és una biblioteca gratuïta i de codi obert
  4. pandas està construït a sobre de NumPy
  5. pandas és una biblioteca d'aprenentatge automàtic

6.- Estructures de dades en pandes Quins són els noms de les estructures de dades suportades actualment a pandas? Seleccioneu totes les opcions correctes.

  1. DataFrame
  2. Panel
  3. Series
  4. DataTable

7.- La funcionalitat de pandes Seleccioneu les accions que es poden realitzar amb la biblioteca pandes:

  1. Exploració i edició de les dades tabulars
  2. Neteja de dades (com ara la gestió dels valors que falten)
  3. Combinació de dades de diverses fonts
  4. Realització d'actualitzacions de bases de dades directament

8.- En el seu lloc Per què és més preferible crear un objecte nou que modificar l'objecte original en pandas?

  1. Perquè la creació d'un objecte nou garanteix un processament més ràpid
  2. Perquè ajuda a depurar el codi i evitar comportaments inesperats
  3. Perquè un objecte nou utilitza menys memòria

1: 1, 2;

2: 3;

3: 1, 4, 5 ;

4: 1;

5: 1, 3, 4;

6: 1, 3;

7: 1, 2, 3;

8: 2;


Series

Els DataFrame es construeixen mitjançant un conjunt d’objectes Series, i normalment treballes amb l’objecte DataFrame. Per això no cal que coneguis en profunditat l’objecte Series.

Pots notar que cada element emmagatzemat en a Series s'associa amb una etiqueta anomenada index. Per defecte, aquest index és només la seqüència 0, 1, 2, ... .

Tanmateix, es pot utilitzar qualsevol valor personalitzat. Per exemple, podem emmagatzemar les edats dels alumnes en un objecte Series, i establir els noms dels estudiants com a identificadors de fila de manera que sabem a quin alumne correspon cada edat:

Està molt bé, però com es crea un objecte Series?

Hi ha diverses maneres de fer-ho.

Conversió d'altres estructures de dades a Sèries

Si les dades ja estan emmagatzemades en una altra estructura de dades (llistes, diciconaris), pots convertir-les fàcilment a un objecte Series com es mostra a continuació:

import pandas as pd

ages_list = [21, 20, 25, 22]
names_list = ['Anna', 'Bob', 'Maria', 'Jack']

ages_series_int8 = pd.Series(ages_list, index=names_list, name='Age', dtype='int8')
print(ages_series_int8)
# Anna     21
# Bob      20
# Maria    25
# Jack     22
# Name: Age, dtype: int8

Aquí, convertim una llista d'edats dels estudiants ages_list en a un objecte Series ages_series_int8. També assignem un índex personalitzat, els noms dels estudiants emmagatzemats a la llista names_list, utilitzant la paraula clau index. Si no proporcionem els valors del index, s'assignarien els valors 0, 1, 2, 3.

Tambè podem especificar el tipus de dades a int8 per estalviar memòria. Com a Numpy, el valor predeterminat que agafa si li posem números és int64. Però si hi hagués una persona de més de 127 anys tindries un error!

Finalment, també podem donar el nostre objecte Series un nom especificant el paràmetre opcional name.

De la mateixa manera, pots convertir un diccionari de Python en un objecte Series. Tingues en compte que les claus del diccionari (els noms dels estudiants a l'exemple següent) es convertiran automàticament en els índexs del nou objecte Series. Genial, oi?

student_ages_dict = {'Anna': 21, 'Bob': 20, 'Maria': 25, 'Jack': 22}
ages_series = pd.Series(student_ages_dict, name='Ages')

print(ages_series)

# Anna     21
# Bob      20
# Maria    25
# Jack     22
# Name: Ages, dtype: int64

Sempre pots canviar més tard l'índex modificant l'atribut de l'índex Series:

ages_series.index = ['A', 'B', 'M', 'J']
print(ages_series)

# A    21
# B    20
# M    25
# J    22
# Name: Ages, dtype: int64

Per descomptat, la longitud de les dades i l'índex que proporcioneu han de coincidir. En cas contrari, es produirà un error.

Sèries amb dates com a índex.

És interessant veure com crear una sèrie amb dates com a índex (index) i strings com a valors (data).

dates = pd.date_range("20240321", periods=6, freq='D')
places = ['Sarria','Portomarín','Palas Do Rei','Arzúa','O Pedrouzo','Santiago']
s2 = pd.Series(data=places,index=dates)
print(dates)
print(s2)

Els dataFrames també permeten els mateixos index que les Series: números, textos, dates ...

Edició de valors d'elements d'un objecte Serie

Pots canviar fàcilment els valors emmagatzemats a un objecte Series, per exemple, accedint-hi per index. Per il·lustrar-ho, actualitzem l'edat de Jack:

student_ages_dict = {'Anna': 21, 'Bob': 20, 'Maria': 25, 'Jack': 22}
ages_series = pd.Series(student_ages_dict, name='Ages', dtype='int16')
ages_series['Jack'] = 23
print(ages_series)

# Anna     21
# Bob      20
# Maria    25
# Jack     23
# Name: Ages, dtype: int64

Al mateix temps, l’objecte Series és immutable respecte la mida; un cop creat, no s'hi poden afegir ni eliminar elements. Es fa d'aquesta manera a propòsit, per emmagatzemar de manera eficient un objecte Series en memòria.

Inserció / esborrat elements d'un objecte Serie

Però què has de fer si necessites afegir o suprimir alguns valors a/des de a Series? No us preocupeu: encara és possible afegir i eliminar elements. Tanmateix, el resultat d'aquestes operacions serà un nou objecte Series.

Per exemple, eliminem el registre de la Maria del nostre Series. Això es pot fer amb el mètode drop:

new_ages_series = ages_series.drop(index='Maria')
print(new_ages_series)
# Anna    21
# Bob     20
# Jack    23
# Name: Ages, dtype: int64

Tingueu en compte que l’objecte original Series no s’ha modificat:

print(ages_series)
# Anna     21
# Bob      20
# Maria    25
# Jack     23
# Name: Ages, dtype: int64

Si vols que enlloc de retornar un nou objecte, es reemplaci l’array de numpy intern, pots utilitzar l’argument opcional inplace:

ages_series.drop(index='Maria', inplace=True)
print(ages_series)

# Anna    21
# Bob     20
# Jack    23
# Name: Ages, dtype: int64

Per afegir nous registres Series, es pot especificar explícitament el valor del nou índex. Afegim el registre de Maria de nou al ages_series:

ages_series['Maria'] = 25 
print(ages_series)

# Anna     21
# Bob      20
# Jack     23
# Maria    25
# Name: Ages, dtype: int64

Si voleu afegir més d'un registre a a Series, serà més ràpid fer-ho amb pd.concat():

new_recs = pd.Series(data={'Jon': 34, 'Peter': 23, 'Karo': 45, 'Abby': 25},dtype='int16')
ages_series = pd.concat([ages_series, new_recs])
print(ages_series)

# Anna     21
# Bob      20
# Jack     23
# Maria    25
# Jon      34
# Peter    23
# Karo     45
# Abby     25
# Name: Ages, dtype: int64

Operacions en sèries

La característica clau de pandas és que podem fer operacions entre diverses Series en que les dades s’alineen automàticament en funció de l'índex.

Imaginem que en tenim dos Series, algebra i calcul, que conté els resultats dels exàmens dels estudiants dels cursos d'Àlgebra i de Càlcul respectivament:

Exam1Serie

Anna     90
Bob      50
Maria     100
Jack    90

Exam2Serie

Anna     100
Bob      90
Jack     70
Maria    80

Suposem que volem calcular la puntuació mitjana que van obtenir els estudiants als dos exàmens.

average = 0.5 * (algebra + calculus)
print(average)

# Anna     75.0
# Bob      90.0
# Jack     80.0
# Maria    90.0
# dtype: float64

Tot i que l'ordre dels alumnes en aquests dos Series és diferent i, tanmateix, les mitjanes es calculen correctament. Molt convenient!

Què passa si alguns índexs d'una Series no existeixen en l'altre Series? Imaginem que tenim el tercer examen de Probabilitat, però només el van fer l'Anna i el Bob.

Què passarà si intentem calcular la mitjana dels tres?

average = 0.33 * (algebra + calculus + probability)
print(average)
# Anna     82.5
# Bob      92.4
# Jack      NaN
# Maria     NaN
# dtype: float64

El resultat d'una operació entre elements no alineats d’un objecte Series és la unió dels índexs implicats. Si no es troba cap etiqueta en un dels operands, el resultat es marcarà com a valor que falta NaN.

Escriure codi sense fer cap alineació explícita de dades us ofereix molta flexibilitat en l'anàlisi de dades. L'alineació de dades integrada en pandas és el que diferencia la biblioteca de la majoria d'eines relacionades per treballar amb dades etiquetades.

Exercicis de sèries.

1.- Resumint dues sèries

Imagineu que tenim dues sèries, series_a i series_b. Aquí estan:

A 1
B 2
C 3
Nom: series_a, dtype: int64
C 1
B 2
D 3
Nom: series_b, dtype: int64

Ara definim series_c com a suma de series_a i series_b: series_c = series_a + series_b

Quin seria l’objecte series_c resultant?

2.- Eliminació d'un registre de la sèrie Quin dels mètodes següents retorna un objecte Series amb les etiquetes d'índex especificades eliminades?

  1. del()
  2. delete()
  3. drop()
  4. remove()

3.- Convertir una llista a Series Suposem que la llista foods conté noms de diferents aliments, i calories és una llista que conté el contingut calòric corresponent per 100 g.

Defineix una funció que crea i retorna un objecte Series que emmagatzema valors del la llista calories fent servir els noms de foods com a índex. A més, defineix el nom de la sèrie com Calorie content.

import pandas as pd
def create_series(foods, calories):
	# write your code here ....

Tingueu en compte que NO cal que imprimiu el resultat. Per exemple, suposem que:

foods = ['bagel', 'pasta', 'rice']
calories = [310, 110, 140]

Aleshores, la vostra funció hauria de retornar el següent Series:

bagel    310
pasta    110
rice     140
Name: Calorie content, dtype: int64

4.- Índex en Series.

Considereu la següent sèrie que conté informació sobre els guanyadors del Festival de la Cançó d'Eurovisió durant els primers cinc anys de la seva existència:

1956 Suïssa
1957 Països Baixos
1958 França
1959 Països Baixos
1960 França
dtype: object

Quin és l'índex aquí?

  1. "Suïssa", "Països Baixos", "França", "Països Baixos", "França"
  2. 0, 1, 2, 3, 4
  3. 1956, 1957, 1958, 1959, 1960
  4. Aquest objecte Series no pot existir ja que Series només pot emmagatzemar dades del mateix tipus.

5.- Eliminació d'elements de la sèrie

Imagina que tens un Series amb el nom olympics que emmagatzema els noms de les ciutats que acullen els Jocs Olímpics d'estiu entre el 2000 i el 2020. L'índex s'estableix en l'any dels jocs (com a nombre enter) i els noms de les ciutats es representen com a cadenes.

Aquí teniu un fragment del Series:

2000            Sydney
2004            Athens
...
2020             Tokyo
dtype: object

Tanmateix, els jocs del 2020 es van cancel·lar a causa del brot de COVID-19.

Defineix una funció que suprimeixi la fila corresponent del fitxer olympics i retorna l’objecte resultant Series.

import pandas as pd
def drop_record(olympics):

6.- Convertir un diccionari en sèrie

Suposem que tens un diccionari capitals que emmagatzema capitals de diferents països (els noms dels països són claus i els noms de les capitals són valors).

Defineix una funció que crea i retorna un objecte Series que emmagatzema valors del capitals amb noms de països com a índexs. A més, estableix el nom del Series com Capitals of the world. Al final, retorneu el resultat Series.

Per exemple, suposem que:

capitals = {'Czech Republic': 'Prague', 
            'Russia': 'Moscow', 
            'Australia': 'Canberra'}

Aleshores, la vostra funció hauria de retornar el següent Series:

Czech Republic      Prague
Russia              Moscow
Australia           Canberra
Name: Capitals of the world, dtype: object

7.- Crear una sèrie amb un índex temporal.

Crea una sèrie que tingui com a índex el primer dia de cada mes en format data, durant 6 mesos. Els valors del dataframe seran 6 números reals que representin la temperatura dels 6 mesos de l'any.

1.- A NaN B 4 C 4 D NaN; 2: 3; 4: 3;

2.- drop()

3.-

import pandas as pd

def create_series(foods, calories):
   series = pd.Series(calories, foods, name="Calorie content")
   return series

foods = ["bagel", "pasta", "rice"]
calories = [310, 110, 140]

df = create_series(foods, calories)
print(df)

4.- Resposta: 3. 1956, 1957, 1958, 1959, 1960

5.-

import pandas as pd
def drop_record(olympics, year):
   olympics = olympics.drop(index=year)
   return olympics

years = ["Atenas", "Sydney", "Athens", "Tokyo"]
ages= [1940, 2000, 2004, 2020]
series = pd.Series(data=years, index=ages)
print(series)
series = drop_record(series, 2020)
series = drop_record(series, 1940)
print(series)

6.-

import pandas as pd
capitals = {"Czech Republic": "Prague", "Russia": "Moscow", "Australia": "Canberra"}

series = pd.Series(capitals, name="Capitals of the world")
print(series)

7.-

dates = pd.date_range("20240101", periods=6, freq='M')
temps = [9.5,8.7,14.3,21.6,19.4,25.2]
s2 = pd.Series(data=temps,index=dates)
print(s2)

DataFrames

Ja estàs familiaritzat amb Series, una estructura de dades unidimensional en pandas. Ara, coneixeràs una altra estructura de dades clau de pandas, que s'anomena DataFrame.

DataFrame és una taula amb columnes. De la mateixa manera que cada element d’un objecte Series està etiquetat amb un índex, cada fila d'un DataFrame està etiquetat amb un índex.

Aquí teniu un exemple d'un objecte DataFrame que emmagatzema informació sobre quatre estudiants:

+----+--------------+---------------+-------+
|    | First Name   | Family Name   |   Age |
|----+--------------+---------------+-------|
|  0 | Anna         | Smith         |    21 |
|  1 | Bob          | Jones         |    20 |
|  2 | Maria        | Williams      |    25 |
|  3 | Jack         | Brown         |    22 |
+----+--------------+---------------+-------+

Aquest DataFrame té tres columnes, és a dir First Name, Family Name, i Age.

Les quatre files estan etiquetades amb índexs 0, 1, 2, 3.

D'acord, però com el creem?

Creació d'un DataFrame: lectura d'un fitxer csv.

Un dels formats de text més populars és .csv, que significa valors separats per comes. Aquest format pot emmagatzemar dades tabulars: cada fila d'un fitxer representa una fila d'una taula, i els valors corresponents a diferents columnes estan separats per comes.

Suposem que les dades sobre els estudiants s'emmagatzemen al fitxer students.csv, el contingut del qual es mostra a continuació:

First Name,Family Name,Age
Anna,Smith,21
Bob,Jones,20
Maria,Williams,25
Jack,Brown,22

Per moure les dades dels estudiants a un DataFrame, pots utilitzar la funció read_csv() de pandas. Aquesta funció pren el camí al fitxer i alguns arguments addicionals que poden ser útils per llegir les dades correctament.

Si volem llegir el fitxer tal com és, simplement podem escriure:

import pandas as pd
students = pd.read_csv('students.csv')
print(students)
+----+--------------+---------------+-------+
|    | First Name   | Family Name   |   Age |
|----+--------------+---------------+-------|
|  0 | Anna         | Smith         |    21 |
|  1 | Bob          | Jones         |    20 |
|  2 | Maria        | Williams      |    25 |
|  3 | Jack         | Brown         |    22 |
+----+--------------+---------------+-------+

Els paràmtres del mètode read_csv més essencials són:

  • sep— el delimitador que s'utilitza (per defecte ,); podem tenir t, ; i altres.
  • header— el número de fila que emmagatzema les capçaleres de columna. Per defecte, pandas intenta inferir-los de la primera fila.
  • names- una llista de noms de columnes. Si voleu utilitzar altres noms de columnes, configureu header=0 i passar una llista de noms de columnes noves amb names.
  • index_col— columnes del fitxer que s'utilitzen com a etiquetes de fila del fitxer DataFrame. Està a punt None per defecte i els números de fila s'utilitzen com a índexs.
  • usecols— una llista de números de columnes o noms de columnes que cal llegir. Per defecte, el DataFrame llegeix totes les columnes.
  • encoding- que per defecte serà utf-8 però podem necessitar canviar-lo.
  • dtypes pandas intenta inferir els tipus de cada columna, però no sempre encerta (pot convertir enters a decimals o pitjor, enters a object) així que si detectem problemes podem exigir-li el tipus.
  • converters pandas intenta inferir els tipus de cada columna, però no sempre encerta (pot convertir enters a decimals o pitjor, enters a object) així que si detectem problemes podem exigir-li el tipus.

Tornem a llegir el mateix fitxer, però aquesta vegada només fem servir la primera i l'última columna, donant-los noms diferents:

students = pd.read_csv('students.csv', usecols=[0,2], header=0, names=['name', 'age'])
print(students)
+----+--------+-------+
|    | name   |   age |
|----+--------+-------|
|  0 | Anna   |    21 |
|  1 | Bob    |    20 |
|  2 | Maria  |    25 |
|  3 | Jack   |    22 |
+----+--------+-------+

Anem a veure un altre exemple de com personalitzar els tipus.

Creem el fitxer cars.csv

Car Name;Price;Condition;Year;Fuel Type
Honda Civic;22000;Used;2021;Gasoline
Ford Mustang;35000;New;2023;Gasoline
Chevrolet Camaro;40000,5;Used;2020;Gasoline
Tesla Model 3;50000;New;2023;Electric
BMW X5;60000;Used;2022;Gasoline
Audi A4;30000;New;2023;Diesel
Toyota Corolla;24999,9;New;2023;Gasoline

Què passa si llegim el fitxer de forma predeterminada ?

import pandas as pd
df_cars = pd.read_csv('./cars.csv')
print(df_cars)
print(df_cars.dtypes)

Que falla de valent!

  File "parsers.pyx", line 2061, in pandas._libs.parsers.raise_parser_error
pandas.errors.ParserError: Error tokenizing data. C error: Expected 1 fields in line 4, saw 2

No perdem la calma, el que passen aquí és que el separador dels camps és ; i no pas la , (el valor predeterminat).

Per tant, hem d'afegir la opció sep=';' a la funció read_csv:

df_cars = pd.read_csv('./cars.csv')

Ara sí que mostra per pantalla el dataframe sencer i els tipus de cada columna.

           Car Name    Price Condition  Year Fuel Type
0       Honda Civic    22000      Used  2021  Gasoline
1      Ford Mustang    35000       New  2023  Gasoline
2  Chevrolet Camaro  40000,5      Used  2020  Gasoline
3     Tesla Model 3    50000       New  2023  Electric
4            BMW X5    60000      Used  2022  Gasoline
5           Audi A4    30000       New  2023    Diesel
6    Toyota Corolla  24999,9       New  2023  Gasoline
Car Name     object
Price        object
Condition    object
Year          int64
Fuel Type    object

A nivell de dtypes de cada columna podem observar que no reconeix el camp Price com a tipus numèric. Això és un problema si volem fer operacions numèriques (com el preu mitjà dels cotxes).

print(df_cars['Price'].mean())
  File "C:\Users\USUARI\miniconda3\envs\bio\Lib\site-packages\pandas\core\nanops.py", line 
720, in nanmean
    the_sum = _ensure_numeric(the_sum)
              ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\USUARI\miniconda3\envs\bio\Lib\site-packages\pandas\core\nanops.py", line 
1701, in _ensure_numeric
    raise TypeError(f"Could not convert string '{x}' to numeric")
TypeError: Could not convert string '220003500040000,550000600003000024999,9' to numeric 

Això ho podem solucionar durant la càrrega amb pandas, però és molt més senzill abordar aquest problema abans de carregar el csv.

import pandas as pd
df_cars = pd.read_csv('./cotxes.csv', sep=';', 
    decimal=',', dtype={'Price': 'float'})

print(df_cars)
print(df_cars.dtypes)
print(f"{df_cars['Price'].mean():.2f}")

Li diem que els decimals en aquest fitxer estan representats per , que no és el més habitual en pandas (decimal predeterminat .).

També ho podriem hacer corregit amb una conversió posterior:

df['Price'] = pd.to_numeric(df['Price'], errors='coerce')

En tot cas, ja podem operar amb el preu dels cotxes i ja tenim la mitjana aritmètica de preus :)

37428.62857142857.2f

El paràmetre index_col specifica quina columna s'ha d'utilitzar com a índex del DataFrame.

Pot prendre un nombre enter o de columna com a valor. En el nostre exemple, podem configurar el Car Name com a columna d'índex, ja que estem segurs que Car Name és una clau primària, amb valors únics i tots definits

df_cars = pd.read_csv('./cars.csv', sep=';', 
    decimal=',', dtype={'Price': 'float'}), index_col='Car Name')

Fixem-nos que ja no ens surt la columna d'index autogenerada:

                    Price Condition  Year Fuel Type
Car Name
Honda Civic       22000.0      Used  2021  Gasoline
Ford Mustang      35000.0       New  2023  Gasoline

Guardar DataFrame a fitxer: escriptura de dades.

Hi ha un (diversos) mètodes anàlegs als que hem vist per guardar els resultats dels nostres dataFrames a fitxers; i comencen per to. Veiem com funciona el to_csv

El fitxer csv contindria:

First Name,Family Name,Age
Anna,Smith,21
Bob,Jones,20
Maria,Williams,25
Jack,Brown,22

El codi que usarem per llegir el fitxer i escriure'n un de nou.

import pandas as pd

# Llegim fitxer csv a dataframe.
df_students: pd.DataFrame = pd.read_csv("students.csv")
print(df_students.info())

# Canviem dades de la primera estudiant.
df_students['Family Name'][0]="Smint"
df_students['Age'][0]=19
print(df_students.head())

# Escrivim dataframe cap a fitxer csv a pandas
df_students.to_csv("new_students.csv",index=False)

En principi s'haura de crear el nou fitxer amb el contingut; i el que es guarda és el següent (hem canviat cognom i edat de la primera estudiant).

El paràmetre index=False l'hem posat perquè no ens cal guardar l'index automàtic del DataFrame.

First Name,Family Name,Age
Anna,Smint,19
Bob,Jones,20
Maria,Williams,25
Jack,Brown,22

Creació d'un DataFrame: lectura d'un fitxer csv d'Internet.

Ja hem explicat com n'és d'important descarregar un fitxer d'Internet via HTTP només si no el tenim descarregat

Ara veurem com carregar dataframes o sèries des de fitxers a Internet amb Pandas.

Mètode 1. Mètode read_csv i to_csv

El mètode read_csv agafa tant rutes de fitxers com url. Internament usa la llibreria requests per fer la descàrrega. És eficient, però no es permet tant de control com urllib3.

import pandas as pd
import os.path
import urllib3

url = "https://gitlab.com/xtec/bio/pandas/-/raw/main/data/persons.csv"
csv_file = "people.csv"

# Descarrega el fitxer (tant si ve del fitxer com si ve de URL)
df = pd.read_csv(csv_file if os.path.isfile(csv_file) else url)

# Guarda el fitxer només si s'ha descarregat de l'URL
if not os.path.isfile(csv_file):
    df.to_csv(csv_file, index=False)
    print(f'Fitxer {csv_file} descarregat i guardat.')

print(df.head())

La crida os.path.isfile(csv_file) descarregarà el fitxer només si no existeix al nostre directori.

Podeu provar que només descarrega el fitxer si no hi ha cap que es digui igual.

Mètode 2. Urllib3

Si voleu tenir més control sobre com es descarrega el fitxer i si hi ha hagut problemes de connectivitat, és millor usar urllib3.

Ja el vam estudiar a la sessió HTTP: https://xtec.dev/python/http/

import pandas as pd
import os.path
import urllib3

url: str = "https://gitlab.com/xtec/bio/pandas/-/raw/main/data/cars.csv?ref_type=heads"
csv_file: str = "cars.csv"

if not os.path.isfile(csv_file):
    http = urllib3.PoolManager()
    response = http.request('GET', url)

    if response.status == 200:
        with open(csv_file, 'wb') as f:
            f.write(response.data)
        print(f"Fitxer {csv_file} descarregat correctament.")
    else:
        print(f"Error en la descàrrega: {response.status}")
else:
    print(f"El fitxer {csv_file} ja existeix.")

# Descarrega el fitxer (tant si ve del fitxer com si ve de URL)
df = pd.read_csv(csv_file if os.path.isfile(csv_file) else url)
print(df.head())

Informació útil del DataFrame: head(), info(), describe().

Primer, creem un exemple de frame de pandas amb informació sobre els cotxes: els seus noms, preus i si estaven trencats.

import pandas as pd
data = {
    'Car Name': ['Toyota Corolla', 'Honda Civic', 'Ford Mustang', 'Chevrolet Camaro', 'Tesla Model 3', 'BMW X5', 'Audi A4'],
    'Price': [25000, 22000, 35000, 40000, 50000, 60000, 30000],
    'Is Broken': [False, True, True, False, True, False, False]
}

df_cars = pd.DataFrame(data)
print(df_cars)
          Car Name  Price  Is Broken
0   Toyota Corolla  25000      False
1      Honda Civic  22000       True
2     Ford Mustang  35000       True
3  Chevrolet Camaro  40000      False
4    Tesla Model 3  50000       True
5           BMW X5  60000      False
6          Audi A4  30000      False

Quan rebeu el vostre DataFrame per primera vegada, heu d'entendre la informació que hi ha.

Hi ha tres mètodes principals per fer-ho: .head(), .info(), .describe().

El mètode .head() de Pandas és un mètode útil per previsualitzar les primeres files d'un DataFrame. S'utilitza habitualment per obtenir una visió general ràpida de les dades i comprendre la seva estructura, els noms de les columnes i els valors reals del DataFrame.

Podeu passar el nombre de files per mostrar com a paràmetre n. Per defecte, només mostrareu 5 files.

df_cars.head(n=3) # display the first 3 rows

        Car Name  Price  Is Broken
0  Toyota Corolla  25000      False
1     Honda Civic  22000       True
2    Ford Mustang  35000       True

El mètode .info() de Pandas s'utilitza per mostrar metainformació sobre un DataFrame: tipus de dades de cada columna, el nombre de valors no nuls, ús de memòria i molt més.

df_cars.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7 entries, 0 to 6
Data columns (total 3 columns):
 #   Column     Non-Null Count  Dtype 
---  ------     --------------  ----- 
 0   Car Name   7 non-null      object
 1   Price      7 non-null      int64 
 2   Is Broken  7 non-null      bool  
dtypes: bool(1), int64(1), object(1)
memory usage: 247.0+ bytes

Quan cridem df.info(), obtenim la informació següent:

  1. <class 'pandas.core.frame.DataFrame'>: indica que df és un DataFrame
  2. RangeIndex: 7 entries, 0 to 6: Mostra que el DataFrame té un índex d'interval de 0 a 6, que indica 7 files.
  3. Data columns (total 3 columns): Indica que el DataFrame té 3 columnes.
  4. Column: Llista els noms de les columnes ("Nom del cotxe", "Preu" i "Està trencat").
  5. Non-Null Count: mostra el nombre de valors no nuls per a cada columna. En el nostre exemple, no hi ha valors nuls.
  6. Dtype: especifica el tipus de dades de cada columna. 'Car Name'és de tipus 'object'(generalment representa cadenes), 'Price' és de tipus 'int64'(enter), i 'Is Broken' és de tipus bool. Tingueu en compte que, de vegades, les columnes es poden inferir de manera incorrecta, de manera que és possible que sigui necessari una inspecció i una conversió de tipus addicionals. Podeu definir els tipus de columnes canviant el dtype durant la lectura del fitxer. Alternativament, podeu utilitzar df.as_type() després de carregar el DataFrame per especificar els tipus correctes.
  7. memory usage: Mostra l'ús de memòria del DataFrame.

Si només us interessen els dtypes podeu usar l’atribut df.dtypes

El mètode .describe() de Pandas s'utilitza per generar un resum d'estadístiques per a columnes numèriques en un DataFrame. Podeu obtenir la mitjana, recompte, std, primer, segon i tercer quantils i altres estadístiques de les dades.

df_cars.describe()

             Price
count      7.000000
mean   37428.571429
std    13709.572603
min    22000.000000
25%    27500.000000
50%    35000.000000
75%    45000.000000
max    60000.000000

Prova de rendiment dels DataFrames.

Sempre és interessant realitzar proves d'estrés del rendiment que ofereixen els dataFrames, així valorem com fer-los més eficients.

Amb aquest codi podem mesurar el tamany i el temps que triga crear un gran dataFrame.

import pandas as pd
import numpy as np

size : int = 2000000

indexes = np.arange(size)
numbers = np.random.normal(size=size,loc=170,scale=10)

pos_names = ['Roser','Jose','Manel','David',
    'Adriana','Efrem','Oliver','Marc','Miquel']
names = np.random.choice(pos_names, size = size)

data = { 'Name' : names, 'Height' : numbers}
df = pd.DataFrame(data, index=indexes)

print(df.info())
print(df)

Exercicis dataframes.

1.- Valors màxims

Baixa el fitxer de https://gitlab.com/xtec/bio/pandas/-/raw/main/data/persons.csv

Importa el fitxer amb pandas i obten el valor màxim de la columna Age.

2.- paràmetre index_col

De quina manera s'utilitza el paràmetre index_col de Pandas?

  1. Estableix la primera columna com a índex del DataFrame.
  2. Especifiqueu la columna d'índex quan llegiu un fitxer JSON.
  3. Establiu una columna d'índex personalitzada per al DataFrame.
  4. Especifiqueu la columna d'índex quan llegiu un fitxer CSV.

3.- Mètodes per visualitzar les dades

Relaciona els mètodes següents amb les seves descripcions:

  • head()
  • info()
  • describe()
  1. Genera un resum d’estadístiques per a columnes numèriques del DataFrame, com ara la mitjana, el recompte, la desviació estàndard i els quartils.

  2. Proporciona una vista prèvia ràpida de les primeres files del DataFrame, ajudant a entendre la seva estructura i els noms de les columnes.

  3. Mostra informació sobre el DataFrame, inclosos els tipus de dades de cada columna, el nombre de valors no nuls i l’ús de la memòria.

4.- Valors nuls

Baixa el fitxer de https://gitlab.com/xtec/bio/pandas/-/raw/main/data/persons.csv

Importa el fitxer amb pandas i digues quin és el nombre de valors no nuls de la columna Address.

5.- Filtra els noms de les files.

Des del mateix fitxer, mostra només les tres primeres files.

2: 2;

3: 1-B, 2-C, 3-A;

1.-

import os.path
import pandas as pd
from urllib.request import urlretrieve

filepath = "persons.csv"

if not os.path.isfile(filepath):
   url = "https://gitlab.com/xtec/bio/pandas/-/raw/main/data/persons.csv"
   urlretrieve(url, filepath)

df = pd.read_csv(filepath)
print(df.describe())

El valor esperat és: 60

4.-

import os.path
import pandas as pd
from urllib.request import urlretrieve

filepath = "persons.csv"

if not os.path.isfile(filepath):
   url = "https://gitlab.com/xtec/bio/pandas/-/raw/main/data/persons.csv"
   urlretrieve(url, filepath)

df = pd.read_csv(filepath)
print(df.info())

El valor esperat és: 281

5.-

...
print(df.head(3))

Creació d'un DataFrame a partir d'altres estructures de dades

També és possible convertir altres estructures de dades, com ara diccionaris, llistes o matrius Numpy, a DataFrame. Heu de passar les dades al constructor DataFrame.

Per exemple, suposem que teniu una llista imbricada que conté informació sobre els estudiants i la volem convertir a DataFrame:

students_list = [['Anna', 'Smith', 21],
                 ['Bob', 'Jones', 20],
                 ['Maria', 'Williams', 25],
                 ['Jack', 'Brown', 22]]

students = pd.DataFrame(students_list, columns = ['First Name', 'Family Name', 'Age'])
print(students)
+----+--------------+---------------+-------+
|    | First Name   | Family Name   |   Age |
|----+--------------+---------------+-------|
|  0 | Anna         | Smith         |    21 |
|  1 | Bob          | Jones         |    20 |
|  2 | Maria        | Williams      |    25 |
|  3 | Jack         | Brown         |    22 |
+----+--------------+---------------+-------+

A més, podríem especificar un altre index en comptes del 0, 1, 2, ... per defecte amb l'argument índex. Provem-ho:

students_index = [100, 200, 300, 400]
students_columns = ['First Name', 'Family Name', 'Age']
students = pd.DataFrame(students_list, 
                        columns = students_columns,
                        index = students_index )
print(students)

Sortida:

+-----+--------------+---------------+-------+
|     | First Name   | Family Name   |   Age |
|-----+--------------+---------------+-------|
| 100 | Anna         | Smith         |    21 |
| 200 | Bob          | Jones         |    20 |
| 300 | Maria        | Williams      |    25 |
| 400 | Jack         | Brown         |    22 |
+-----+--------------+---------------+-------+

En crear un DataFrame d'un diccionari imbricat, els noms d'índex i de columnes s'inferiran automàticament a partir de les claus del diccionari.

Mireu l'exemple:

# This is a nested dictionary representing the students table
students_dict = {'First Name': {100: 'Anna', 
                                200: 'Bob', 
                                300: 'Maria',
                                400: 'Jack'},
                 
                 'Family Name': {100: 'Smith', 
                                 200: 'Jones',
                                 300: 'Williams',
                                 400: 'Brown'},
                 'Age': {100: 21, 
                         200: 20, 
                         300: 25,
                         400: 22}}

students = pd.DataFrame(students_dict)
print(students)

Sortida:

+-----+--------------+---------------+-------+
|     | First Name   | Family Name   |   Age |
|-----+--------------+---------------+-------|
| 100 | Anna         | Smith         |    21 |
| 200 | Bob          | Jones         |    20 |
| 300 | Maria        | Williams      |    25 |
| 400 | Jack         | Brown         |    22 |
+-----+--------------+---------------+-------+

Shape --> Forma dels dataframes

Per comprovar quantes files i columnes té un Dataframe, pots accedir a l’atribut shape com feiem amb Numpy.

Conté una tupla amb dos valors, les dimensions al llarg dels dos eixos. Per exemple, en el nostre alumne DataFrame, hi ha quatre files i tres columnes:

students.shape
# (4, 3)

Selecció de primeres (head) i últimes (tail) files.

El DataFrame pot ser massa gran per imprimir-lo. En aquest cas, utilitza els mètodes head() i tail().

Imprimiran la primera o les últimes cinc files del DataFrame respectiu. Si voleu que es mostri un nombre diferent de files, només heu d'especificar-lo entre parèntesis.

Imprimim només les dues primeres files:

students.head(2)
+-----+--------------+---------------+-------+
|     | First Name   | Family Name   |   Age |
|-----+--------------+---------------+-------|
|   0 | Anna         | Smith         |    21 |
|   1 | Bob          | Jones         |    20 |
+-----+--------------+---------------+-------+

Selecció de columnes.

També podeu accedir a cadascuna de les columnes del DataFrame per separat posant el nom de la columna entre claudàtors després del nom del DataFrame. Tingueu en compte que cada columna del DataFrame és un objecte Series:

students['Age']
# 0    21
# 1    20
# 2    25
# 3    22
# Name: Age, dtype: int64

Si necessiteu accedir a diverses columnes alhora, només heu de posar els seus noms en una llista. Mirem només la primera i l'última columnes. Tingueu en compte que la taula resultant és un nou objecte DataFrame:

students[['First Name', 'Age']]
+----+--------------+-------+
|    | First Name   |   Age |
|----+--------------+-------|
|  0 | Anna         |    21 |
|  1 | Bob          |    20 |
|  2 | Maria        |    25 |
|  3 | Jack         |    22 |
+----+--------------+-------+

Tingueu en compte també que si voleu obtenir una sola columna d'un DataFrame com un altre objecte DataFrame enlloc d’un objecte Series, hauríeu de posar el nom de les columnes entre claudàtors dobles:

students[['Age']]
+----+-------+
|    |   Age |
|----+-------|
|  0 |    21 |
|  1 |    20 |
|  2 |    25 |
|  3 |    22 |
+----+-------+

Organització de dades als DataFrames

Ja hem comentat quin tipus de dades es poden emmagatzemar en un DataFrame i com es poden crear.

Ara, anem a saber com podem modificar un DataFrame existent. En aquest tema, parlarem d'algunes operacions bàsiques, com ara canviar el nom, reordenar columnes o canviar l'índex.

Accés als eixos de DataFrame

En primer lloc, hem d'importar pandas i crear un DataFrame d'un diccionari:

import pandas as pd

pets = {
    'species': ['cat', 'dog', 'parrot', 'cockroach'], 
    'name': ['Dr. Mittens Lamar', 'Diesel', 'Peach', 'Richard'], 
    'legs': [4, 4, 2, 6],
    'wings': [0, 0, 2, 4],
    'looking_for_home': ['no', 'no', 'no', 'yes']
}
df = pd.DataFrame(pets)
df.head()

Aquí teniu la sortida:

+----+-----------+-------------------+--------+---------+--------------------+
|    | species   | name              |   legs |   wings | looking_for_home   |
|----+-----------+-------------------+--------+---------+--------------------|
|  0 | cat       | Dr. Mittens Lamar |      4 |       0 | no                 |
|  1 | dog       | Diesel            |      4 |       0 | no                 |
|  2 | parrot    | Peach             |      2 |       2 | no                 |
|  3 | cockroach | Richard           |      6 |       4 | yes                |
+----+-----------+-------------------+--------+---------+--------------------+

Podem canviar els índexs en tots dos DataFrames i Series. Els índexs poden utilitzar diferents tipus de dades, com ara cadenes, objectes Datetime, nombres flotants, valors booleans i altres.

Podeu veure l'índex de files a la primera columna de l'esquerra. Els noms de les columnes ( etiquetes ) es troben a la capçalera. Una altra manera de descriure la indexació és l'etiquetatge d'eixos.

Podeu veure dos eixos al nostre marc de dades, verticals (files) — axis 0 i horitzontal (columnes) — axis 1.

Fem una ullada als eixos del nostre DataFrame accedint a df.axes.

Això és el que obtindrem:

[RangeIndex(start=0, stop=4, step=1),
 Index(['species', 'name', 'legs', 'wings', 'looking_for_home'], dtype='object')]

El primer objecte de la llista és el mètode d'indexació per a files i el segon per a columnes.

La manera predeterminada d'indexar les dades que contenen n files és utilitzant un interval d'enters 0, 1, 2, 3,..., n−1.

Aquest índex reflecteix les posicions dels elements. Com podeu veure més amunt, el nostre DataFrame utilitza només aquest tipus d'indexació de files (interval d'enters): la primera fila té el 0 índex, l'última fila té índex 3.

Comprovem la sortida del mètode df.info():

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4 entries, 0 to 3
Data columns (total 5 columns):
 #   Column            Non-Null Count  Dtype 
---  ------            --------------  ----- 
 0   species           4 non-null      object
 1   name              4 non-null      object
 2   legs              4 non-null      int64 
 3   wings             4 non-null      int64 
 4   looking_for_home  4 non-null      object
dtypes: int64(2), object(3)
memory usage: 288.0+ bytes

Com podeu veure, la primera línia descriu la classe d'objecte ( DataFrame), després el tipus de dades per a la indexació i, a continuació, hi ha una llista de columnes que conté índexs de columnes posicionals, etiquetes de columnes, Non-null Count (el nombre de files no buides) i dtype (un tipus de dades, es detecta automàticament com a object per Pandas).

L'objecte d'índex de fila s'emmagatzema df.index

Podem veure l'índex actual cridant l'atribut corresponent al marc de dades:

df.index
RangeIndex(start=0, stop=4, step=1)

Com que no hi ha etiquetes de fila, l'atribut retornarà un interval d'enters. Podeu aconseguir el mateix resultat utilitzant df.axes[0].

Consell: .info() també us ofereix índexs posicionals. A més de la indexació posicional, de vegades és útil utilitzar etiquetes personalitzades.

Per veure les etiquetes de columna d'a DataFrame, utilitzar df.columns:

Index(['species', 'name', 'legs', 'wings', 'looking_for_home'], dtype='object')

Configurar, canviar i restablir un índex

Una manera de canviar els noms de les columnes és assignar un nou valor a la columns atribut. El nou valor ha de tenir la mateixa longitud que el nombre de columnes.

Canviem el valor d'algunes columnes assignant una llista de nous valors a l'atribut columns:

df.columns = ['col', 'col2', 'col3', 'col4',  'col5']
df.head()

Aquí teniu la sortida:

+----+-----------+-------------------+--------+--------+--------+
|    | col       | col2              |   col3 |   col4 | col5   |
|----+-----------+-------------------+--------+--------+--------|
|  0 | cat       | Dr. Mittens Lamar |      4 |      0 | no     |
|  1 | dog       | Diesel            |      4 |      0 | no     |
|  2 | parrot    | Peach             |      2 |      2 | no     |
|  3 | cockroach | Richard           |      6 |      4 | yes    |
+----+-----------+-------------------+--------+--------+--------+

Com podeu veure, les columnes ara tenen noms diferents. Podem assignar una nova llista d'etiquetes al index atribut:

df.index = ['row', 'row2', 'row3', 'row4']
df.head()

Així serà la taula:

+------+-----------+-------------------+--------+--------+--------+
|      | col       | col2              |   col3 |   col4 | col5   |
|------+-----------+-------------------+--------+--------+--------|
| row  | cat       | Dr. Mittens Lamar |      4 |      0 | no     |
| row2 | dog       | Diesel            |      4 |      0 | no     |
| row3 | parrot    | Peach             |      2 |      2 | no     |
| row4 | cockroach | Richard           |      6 |      4 | yes    |
+------+-----------+-------------------+--------+--------+--------+

També podeu utilitzar qualsevol columna com a índex. Indexem les nostres dades pel nom. Ho podem fer amb el mètode set_index().

Així que podem assignar un nou DataFrame objecte al nostre df variable o utilitzeu un argument opcional inplace=True.

Aquesta funció alterarà totalment el Dataframe existent (internament es crearà un nou Dataframe i s'esborrarà l'anterior).

Tornem al nostre DataFrame i restablir el seu índex.

df.set_index('name', inplace=True)  
# is equivalent to df = df.set_index('name')
df.head()

Aquí teniu la sortida:

+-------------------+-----------+--------+---------+--------------------+
|                   | species   |   legs |   wings | looking_for_home   |
|-------------------+-----------+--------+---------+--------------------|
| name              |           |        |         |                    |
|-------------------+-----------+--------+---------+--------------------|
| Dr. Mittens Lamar | cat       |      4 |       0 | no                 |
| Diesel            | dog       |      4 |       0 | no                 |
| Peach             | parrot    |      2 |       2 | no                 |
| Richard           | cockroach |      6 |       4 | yes                |
+-------------------+-----------+--------+---------+--------------------+

Ara, la indexació es basa en la columna name. Si mirem l'atribut index que s'utilitza ara df.index, podem veure que va canviar de rang a la llista de noms:

Index(['Dr. Mittens Lamar', 'Diesel', 'Peach', 'Richard'], dtype='object', name='name')

Consell: Només l’objecte DataFrames té el mètode .set_index().

Podem tornar a restablir la columna d'índex al valor predeterminat (interval sencer) fent servir reset_index().

Com s'ha esmentat anteriorment, utilitzeu inplace=True per guardar els canvis:

df.reset_index(inplace=True)

Sortida:

+----+-------------------+-----------+--------+---------+--------------------+
|    | name              | species   |   legs |   wings | looking_for_home   |
|----+-------------------+-----------+--------+---------+--------------------|
|  0 | Dr. Mittens Lamar | cat       |      4 |       0 | no                 |
|  1 | Diesel            | dog       |      4 |       0 | no                 |
|  2 | Peach             | parrot    |      2 |       2 | no                 |
|  3 | Richard           | cockroach |      6 |       4 | yes                |
+----+-------------------+-----------+--------+---------+--------------------+

Un cop hem restablert l'índex, la columna name passa a ser la primera. Si voleu reindexar les vostres dades i suprimir els índexs existents, feu servir drop=True.

Canviar el nom de columnes

També podeu utilitzar el mètode .rename() per canviar el nom de les columnes. Només cal passar un diccionari amb noms de columnes antics com a claus i noms de columnes noves com a valors:

df.rename(columns={'name': 'pet_name', 'looking_for_home': 'homeless'}, inplace=True)
df.head()

Aquí teniu la sortida:

+----+-------------------+-----------+--------+---------+------------+
|    | pet_name          | species   |   legs |   wings | homeless   |
|----+-------------------+-----------+--------+---------+------------|
|  0 | Dr. Mittens Lamar | cat       |      4 |       0 | no         |
|  1 | Diesel            | dog       |      4 |       0 | no         |
|  2 | Peach             | parrot    |      2 |       2 | no         |
|  3 | Richard           | cockroach |      6 |       4 | yes        |
+----+-------------------+-----------+--------+---------+------------+

Com podeu veure, tot és molt còmode. No cal esmentar totes les columnes si només volem canviar el nom d'algunes d'elles.

També pots utilitzar .rename() per canviar els noms índexs: només cal passar el index = {...} argument en lloc de columns={...}.


Sistemes de coordenades Dataframes: .iloc i .loc

Podem accedir a una peça d'informació emmagatzemada en una fila o una columna concreta en lloc de treballar amb tota la informació del Dataframe.

Ho fem amb indexació i podem seleccionar un subconjunt particular de a DataFrame o a Series per treballar-hi.

En un dataframe, el sistema de coordenades, comença per 0, i la coordenada s'indica primera la fila i després la columna.

Regla nemotècnica (enfonsar-se i nedar)

  1. Primer et tires de cap a la posició de la fila que vols.

  2. Després, vas nedant fins la columna que t'interessa.

Abans de començar, importem pandas (abreujat com pd) i creem un DataFrame d'un diccionari:

import pandas as pd
people = {
    "first_name": ["Michael", "Michael", 'Jane', 'John'], 
    "last_name": ["Jackson", "Jordan", 'Doe', 'Doe'], 
    "email": ["mjackson@email.com", "mjordan@email.com", 
'JaneDoe@email.com', 'JohnDoe@email.com'],
    "birthday": ["29.09.1958", "17.02.1963", "15.03.1978", "12.05.1979"],
    "height": [1.75, 1.98, 1.64, 1.8]
}
df = pd.DataFrame(people)
df.head()

Aquí teniu la sortida:

 first_name last_name               email    birthday  height
0    Michael   Jackson  mjackson@email.com  29.09.1958    1.75
1    Michael    Jordan   mjordan@email.com  17.02.1963    1.98
2       Jane       Doe   JaneDoe@email.com  15.03.1978    1.64
3       John       Doe   JohnDoe@email.com  12.05.1979    1.80

pandas ofereix dues funcions addicionals per seleccionar un subconjunt de files i columnes: .loc i .iloc

Tingueu en compte que les dues característiques no són mètodes: són propietats de Python i, per això, utilitzen claudàtors. Primer, recordeu que la seva sintaxi bàsica és similar:

.loc[<row selection>, <optional column selection>]
.iloc[<row selection>, <optional column selection>]

.iloc

Comencem amb .loc; el més habitual.

Pot gestionar índexs basats en nombres enters com a etiquetes, però per a més claredat, crearem i anomenarem un índex de text:

df.index = ['first', 'second', 'third', 'fourth']
df.index.name = 'index'
df.head()

Sortida:

      first_name last_name               email    birthday  height
index                                                              
first     Michael   Jackson  mjackson@email.com  29.09.1958    1.75
second    Michael    Jordan   mjordan@email.com  17.02.1963    1.98
third        Jane       Doe   JaneDoe@email.com  15.03.1978    1.64
fourth       John       Doe   JohnDoe@email.com  12.05.1979    1.80

.loc pot prendre:

  • una sola etiqueta de fila;
  • una llista d'etiquetes de files;
  • un tros d'etiquetes de fila;
  • un resultat de declaracions condicionals (una matriu booleana)

També podríem passar columnes com a segon argument d'una manera similar: una sola etiqueta, una llista o una porció.

Si passem un sol argument, pandas tornarà a Series:

df.loc['third']
Sortida:
first_name                 Jane
last_name                   Doe
email         JaneDoe@email.com
birthday             15.03.1978
height                     1.64
Name: third, dtype: object

També podeu seleccionar una sola cel·la:

df.loc['third', 'last_name']

Sortida:

'Doe'

Com podeu veure, hem retornat un valor de cel·la. En aquest cas, és del tipus String.

Per passar una llista d'etiquetes, hem de fer el següent:

df.loc[['first','fourth']]

Obtenim les files amb el primer i el quart índex:

      first_name last_name               email    birthday  height
index                                                              
first     Michael   Jackson  mjackson@email.com  29.09.1958    1.75
fourth       John       Doe   JohnDoe@email.com  12.05.1979    1.80

Afegim una llista de columnes d'etiquetes:

df.loc[['first','fourth'], ['last_name', 'birthday']]

Sortida:

      last_name    birthday
index                       
first    Jackson  29.09.1958
fourth       Doe  12.05.1979
print(df.loc[['first','fourth']]['email'])
index
first 	mjackson@email.com
fourth 	JohnDoe@email.com
Name: email, dtype: object

Tingueu en compte que la primera llista dins del loc els claudàtors defineixen la selecció de fila mentre que la segona llista defineix la selecció de columnes.

Aquí ve una porció d'etiquetes de fila:

df.loc['second':'fourth']

Sortida:

         first_name last_name              email    birthday  height
index                                                             
second    Michael    Jordan  mjordan@email.com  17.02.1963    1.98
third        Jane       Doe  JaneDoe@email.com  15.03.1978    1.64
fourth       John       Doe  JohnDoe@email.com  12.05.1979    1.80

Igual que abans, podem introduir una condició (amb una porció de columna):

df.loc[df.birthday == '12.05.1979', 'last_name':'birthday':2]

Sortida:

      last_name    birthday
index                       
fourth       Doe  12.05.1979

El primer argument aquí pren una fila mentre la columna d'aniversari s'estableix a 12.05.1979. El segon argument pren columnes de last_name a birthday amb un pas de 2.

És a dir, triga cada segona columna, començant per la primera seleccionada.

No dubteu a triar qualsevol combinació de valors, llistes i seccions individuals.

result = df.loc[df.height > 1.76]
print(result)

Sortida:

   	first_name last_name          	email	birthday  height
index                                                        	 
second	Michael	Jordan  mjordan@email.com  17.02.1963	1.98
fourth   	John   	Doe  JohnDoe@email.com  12.05.1979	1.80

Exercici loc.

Com podem obtenir el nom de la persona amb la alçada mínima ?

# Pas 1. Obtenir el mínim
minim = df.height.min()

# Pas 2. Obtenir el nom del mínim, sabent el mínim
result = df.loc[df.height == minim]

.iloc

La sintaxi bàsica d'iloc és la mateixa que a loc, però aquesta se centra en els índexs enters ordinals; aquí no podem utilitzar condicionals.

Per tant, torneu a l'inici DataFrame restablint i deixant anar l'índex d'etiquetes; ja no en necessitem:

df.reset_index(drop=True, inplace=True)
df.head()

Sortida:

  first_name last_name               email    birthday  height
0    Michael   Jackson  mjackson@email.com  29.09.1958    1.75
1    Michael    Jordan   mjordan@email.com  17.02.1963    1.98
2       Jane       Doe   JaneDoe@email.com  15.03.1978    1.64
3       John       Doe   JohnDoe@email.com  12.05.1979    1.80

Al principi, seleccionem el valor de la primera fila i columna:

df.iloc[0, 0]

Retorna la cel·la:

'Michael'

També podem seleccionar quatre cel·les internes:

df.iloc[[1, 2], [1, 2]]

Sortida:

 last_name              email
1    Jordan  mjordan@email.com
2       Doe  JaneDoe@email.com

No t'oblidis del pas! Per definir un pas k dins d'un interval de fila [x,y], utilitzeu la sintaxi següent:

df.iloc[x:y:k, :]

Per exemple, podem enumerar cada segona fila (a partir de zero) amb aquesta línia de codi:

df.iloc[::2, :]

Sortida:

 first_name last_name               email    birthday  height
0    Michael   Jackson  mjackson@email.com  29.09.1958    1.75
2       Jane       Doe   JaneDoe@email.com  15.03.1978    1.64

Genial, no? Aquesta tècnica sembla senzilla si ja esteu familiaritzat amb les llistes de Python.

Tingues en compte que .iloc pren una posició entera. Vol dir que si no tenim una numeració de línia d'extrem a extrem, prendrà les posicions de la fila.

Així que si tenim una indexació fantàstica com aquesta:

   a  b
10  1  4
0   2  5
20  3  6

df.iloc[0] encara seleccionarà la primera fila (amb un índex de 10):

a    1
b    4
Name: 10, dtype: int64

I df.loc[0]seleccionarà la segona fila (amb un índex de 0):

a    2
b    5
Name: 0, dtype: int64

Ús .loc i .iloc quan es vol canviar una part DataFrame.

En resum, anem a veure les principals diferències entre .loci .iloc en una taula:

.loc .iloc
Selecció de fila condicional No
Pren files com Noms d'índex Posició entera de l'índex
Pren les columnes com a Noms de columnes Posició entera de la columna

Modificació d'un DataFrame amb loc & iloc

Tots dos mètodes no només són una manera convenient de seleccionar una part d'a DataFrame, però també ajuden a modificar una part d'a DataFrame només amb una línia de codi.

Imaginem una situació: per desar dades personals en un servidor, els usuaris us han d'enviar un Acord de tractament de dades (DPA).

Suposem que no has rebut el DPA de Jane i John Doe.

Actualitzem les nostres dades:

df.iloc[2:, 2:5] = "no DPA"

Això és el que obtindrem:

 first_name last_name               email    birthday  height
0    Michael   Jackson  mjackson@email.com  29.09.1958    1.75
1    Michael    Jordan   mjordan@email.com  17.02.1963    1.98
2       Jane       Doe              no DPA      no DPA  no DPA
3       John       Doe              no DPA      no DPA  no DPA

Selecció condicional

De vegades, és possible que vulguem accedir a una informació emmagatzemada en una fila o una columna concreta en lloc de treballar amb un DataFrame sencer.

La bona notícia és que Pandas ens permet fer-ho. S'anomena indexació ; podem seleccionar un subconjunt particular de a DataFrame o a Series per treballar-hi.

Selecció

Abans de començar, importem pandas(abreujat com pd) i creeu un DataFrame a partir d'un diccionari:

import pandas as pd

people = {
    "first_name": ["Michael", "Michael", 'Jane', 'John'], 
    "last_name": ["Jackson", "Jordan", 'Doe', 'Doe'], 
    "email": ["mjackson@email.com", "mjordan@email.com", 'JaneDoe@email.com', 'JohnDoe@email.com'],
    "birthday": ["29.09.1958", "17.02.1963", "15.03.1978", "12.05.1979"],
    "height": [1.75, 1.98, 1.64, 1.8]
}
df = pd.DataFrame(people)
df.head()

Sortida:

+----+--------------+-------------+--------------------+------------+----------+
|    | first_name   | last_name   | email              | birthday   |   height |
|----+--------------+-------------+--------------------+------------+----------|
|  0 | Michael      | Jackson     | mjackson@email.com | 29.09.1958 |     1.75 |
|  1 | Michael      | Jordan      | mjordan@email.com  | 17.02.1963 |     1.98 |
|  2 | Jane         | Doe         | JaneDoe@email.com  | 15.03.1978 |     1.64 |
|  3 | John         | Doe         | JohnDoe@email.com  | 12.05.1979 |     1.8  |
+----+--------------+-------------+--------------------+------------+----------+

Podem seleccionar qualsevol subconjunt del DataFrame, per exemple, una sola columna:

df['email']

Sortida:

0    mjackson@email.com
1     mjordan@email.com
2     JaneDoe@email.com
3     JohnDoe@email.com
Name: email, dtype: object

Ara tenim un Pandas sèrie amb correus electrònics.

També és possible utilitzar-lo df.email. S'anomena sintaxi de punts.

El podem utilitzar per a noms de columnes sense espais.

També podem seleccionar una llista de columnes. Una llista de Python requereix claudàtors addicionals:

df[['first_name', 'last_name']]

Sortida:

+----+--------------+-------------+
|    | first_name   | last_name   |
|----+--------------+-------------|
|  0 | Michael      | Jackson     |
|  1 | Michael      | Jordan      |
|  2 | Jane         | Doe         |
|  3 | John         | Doe         |
+----+--------------+-------------+

Ara tenim un nou DataFrame, format per aquestes dues columnes. Aquesta ordre sempre retorna un DataFrame, de manera que, fins i tot si seleccioneu una llista que consta d'una columna, tornareu un DataFrame:

df[['last_name']]

Sortida:

+----+-------------+
|    | last_name   |
|----+-------------|
|  0 | Jackson     |
|  1 | Jordan      |
|  2 | Doe         |
|  3 | Doe         |
+----+-------------+

Condicionals

Si hem d'introduir una condició per refinar la nostra elecció, podem incloure-la entre claudàtors:

df[df.height < 1.8]

Sortida:

+----+--------------+-------------+--------------------+------------+----------+
|    | first_name   | last_name   | email              | birthday   |   height |
|----+--------------+-------------+--------------------+------------+----------|
|  0 | Michael      | Jackson     | mjackson@email.com | 29.09.1958 |     1.75 |
|  2 | Jane         | Doe         | JaneDoe@email.com  | 15.03.1978 |     1.64 |
+----+--------------+-------------+--------------------+------------+----------+

Com a resultat, hem retornat totes les columnes i files on height el valor és inferior a 1,8.

Tingueu en compte que és possible utilitzar claudàtors dins d'aquesta declaració, per exemple, df[df['height'] < 1.8], però no cal que ho fem aquí, ja que el nom de la columna no conté espais en blanc.

Si necessitem combinar diverses condicions, utilitzem els següents operadors booleans:

  • & per "i"
  • | (línia vertical) per "o"
  • ~ per "no"
  • , <, >=, <=, ==, !=per comparar declaracions.

Si us plau, no us oblideu del parèntesi:

df[(df.first_name == 'Michael') & (df.birthday == '17.02.1963')]

Sortida:

+----+--------------+-------------+-------------------+------------+----------+
|    | first_name   | last_name   | email             | birthday   |   height |
|----+--------------+-------------+-------------------+------------+----------|
|  1 | Michael      | Jordan      | mjordan@email.com | 17.02.1963 |     1.98 |
+----+--------------+-------------+-------------------+------------+----------+

Com a resultat, vam tornar les files que coincideixen amb la nostra condició preestablerta: "el primer nom és Michael, l'aniversari és el 17.02.1963".

Un exemple més amb una condició una mica més complexa. Definim el primer nom com Michael o John, una alçada igual o superior a 1,8. El cognom no hauria de ser Jordan:

print(df[((df.first_name == 'Michael') | (df.first_name == 'John'))
   & (df.height >= 1.8)
   & (df.last_name != 'Jordan')])

Sortida:

+----+--------------+-------------+-------------------+------------+----------+
|    | first_name   | last_name   | email             | birthday   |   height |
|----+--------------+-------------+-------------------+------------+----------|
|  3 | John         | Doe         | JohnDoe@email.com | 12.05.1979 |      1.8 |
+----+--------------+-------------+-------------------+------------+----------+

Tingueu en compte que no fem servir el caràcter de salt de línia , ja que totes les línies noves es posen dins dels claudàtors.

Si volem fer exclusiu el nostre filtratge , és a dir, seleccionar-ho tot excepte els paràmetres indicats, podem afegir un caràcter de tilde ~i parèntesis addicional:

df[~(((df.first_name == 'Michael') | (df.first_name == 'John'))
   & (df.height >= 1.8)
   & (df.last_name != 'Jordan'))]

Sortida:

+----+--------------+-------------+--------------------+------------+----------+
|    | first_name   | last_name   | email              | birthday   |   height |
|----+--------------+-------------+--------------------+------------+----------|
|  1 | Michael      | Jordan      | mjordan@email.com  | 17.02.1963 |     1.98 |
+----+--------------+-------------+--------------------+------------+----------+

Filtrem amb queries.

Les versions de Pandas 1.3 i posteriors permeten filtrar amb una altra sintaxi, semblant al SQL.

Aquesta consulta:

 df[((df.first_name == 'Michael') | (df.first_name == 'Jordan'))

És equivalent a:

print(df.query("first_name=='Michael' and last_name=='Jordan'"))
print(df.query("height>1.70 and height<1.86"))

Camps categorical. Millora del rendiment dels strings.

Quan ens trobem amb un camp que pot tenir uns pocs valors, que es podrien representar en una llista de selecció (el select d’html) ens interessa crear un camp categorical.

Exemples de camps categòrics poden ser: gènere, grup sanguini, estat civil, continent del món, Sí/No/NoContesta, etc...

Per crear un categòric de gènere ho podem fer de dues maneres.

Mètode 1. Amb el constructor pd.Categorical:

Si ja sabem tots els possibles valors categòrics del dataset, com pot ser el camp gender, el que ofereix millor rendiment és aquest constructor.

gender = pd.Categorical(["Male", "Female", "Non-Binary", "Transgender", "Intersex", "I prefer not to say"])

Mètode 2. Amb la funció astype:

Si volem transformar una columna d’strings (dtype object per defecte) a categorical hem d’usar els mètodes astype; suposem el següent exemple:

csv_file:str = "drinks.csv"
if not os.path.isfile(csv_file):
    urlretrieve('http://bit.ly/drinksbycountry',csv_file)
drinks = pd.read_csv(csv_file)

print("Datatype of each column:")
print(drinks.dtypes)
drinks['continent'] = drinks.continent.astype('category')
print("\nDatatype after creating category column:")
print(drinks.dtypes)

Després d'executar aquest codi, veuràs que la columna continent ha canviat el seu tipus de dades a category, el que permet optimitzar memòria i millorar el rendiment en operacions com el recompte d'ocurrències.

Una variant d'aquest mètode és crear camps categòrics quan llegim el dataframe o serie usant el mètode dtype:

drinks = pd.read_csv('http://bit.ly/drinksbycountry', 
                    dtype={'continent':'category'})
print("Datatype of each column:")
print(drinks.dtypes)

A nivell de rendiment, aquest mètode no és tant eficient com el constructor, perquè pandas ha de deduïr un per un de quin tipus és cada valor. Però per a programar és més còmode.


Recompte d’ocurrències de cada valor columna.

Per analitzar de forma ràpida grans volums de dades ens serà molt útil comptar les ocurrències de cada valor en cada columna, sobretot si són categòrics o numèrics discrets (pocs possibles números)

Funcions com unique, value_counts, i altres que poden ajudar a entendre millor els valors de les columnes del dataframe drinks.

Seguim amb l'exemple de les begudes:

import pandas as pd
import os
from urllib.request import urlretrieve

csv_file = "drinks.csv"
if not os.path.isfile(csv_file):
    urlretrieve('http://bit.ly/drinksbycountry', csv_file)

drinks = pd.read_csv(csv_file)
drinks['continent'] = drinks.continent.astype('category')

unique El mètode unique() et permet veure tots els valors únics d'una columna. Això és útil si vols saber quins són els valors diferents que existeixen en una columna.

Exemple: Quins continents únics tenim?

# Quins continents únics tenim?
print(drinks['continent'].unique())

Sortida:

['Asia', 'Europe', 'Africa', 'North America', 'South America', 'Oceania']

value_counts El mètode value_counts() compta la quantitat de vegades que apareix cada valor d'una columna. La estructura que et retorna és una Serie. Aquest és un dels mètodes més útils per explorar dades, ja que et dóna una idea de com es distribueixen els valors.

Exemple: Comptem quants països hi ha per cada continent

print(drinks['continent'].value_counts())

Sortida:

Africa           53
Europe           45
Asia             44
North America    23
South America    12
Oceania          16
Name: continent, dtype: int64

Això ens diu quants països corresponen a cada continent. Si hi hagués algun valor no vàlid o missings (com nan), no es comptarien per defecte, però pots incloure'ls utilitzant dropna=False:

print(drinks['continent'].value_counts(dropna=False))

Això inclouria la comptabilitat de valors NaN (si n'hi haguessin).

sort_values Una vegada tens els valors amb value_counts(), pots ordenar-los utilitzant sort_values(). Això és útil si vols veure els valors en ordre ascendent o descendent de freqüència.

Exemple:

# Ordenar els continents per nombre de països de menor a major
print(drinks['continent'].value_counts().sort_values())

Sortida possible:

South America    12
Oceania          16
North America    23
Asia             44
Europe           45
Africa           53
Name: continent, dtype: int64

Això t'ajuda a veure ràpidament quins continents tenen menys països representats en el conjunt de dades.

Aquests mètodes funcionen igual de bé amb qualsevol altra columna. Per exemple, podries veure quins valors diferents hi ha a la columna beer_servings, i quants països tenen un nombre específic de beer_servings:

# Quins valors únics hi ha a beer_servings?
print(drinks['beer_servings'].unique())

# Comptem quantes vegades apareix cada valor de beer_servings
print(drinks['beer_servings'].value_counts())

replace, remplaçar valors en columnes:

Com que replace() és molt útil per modificar valors específics, pots utilitzar-lo per canviar el nom d'equips o qualsevol valor en el dataframe.

Exemple:

# Reemplacem el nom d'alguns continents (fictici exemple)
drinks['continent'] = drinks['continent'].replace({'Europe': 'EU'})
print(drinks['continent'].value_counts())

Anem a provar un altre exemple, suposem que en comptes d'equips amb nom 'A', 'B' i 'C' els volem reemplaçar per altres noms. Endavant!

import pandas as pd
df = pd.DataFrame({'team': ['A', 'A', 'B', 'B', 'B', 'B', 'B', 'C'],
                   'points': [15, 12, 18, 20, 22, 28, 35, 40]})
df['team'] = df['team'].replace({'A':'Athletic','B':'Barça','C':'Celta'})
print(df)

Matplotlib i pandas.

Matplotlib permet crear gràfics amb Series i DataFrames molt fàcilment.

En aquest punt veurem 2 exemples senzills, i a la secció d'estadística veurem uns quants exemples més.

Gràfic de barres. Recompte d'ocurrències d'una variable.

Agafem l'exemple de les begudes, per a fer un gràfic de barres del número de països de cada continent (una Serie) que s'han inscrit a l'estudi.

import pandas as pd
import matplotlib.pyplot as plt
import os
from urllib.request import urlretrieve

csv_file = "drinks.csv"
if not os.path.isfile(csv_file):
    urlretrieve('http://bit.ly/drinksbycountry', csv_file)

drinks = pd.read_csv(csv_file)
drinks['continent'] = drinks.continent.astype('category')

continent_counts = drinks['continent'].value_counts()

plt.figure(figsize=(8, 6))
continent_counts.plot(kind='bar', color='skyblue')

plt.title("Nombre de països de l'estudi de begudes per continent", fontsize=14)
plt.xlabel("Continent", fontsize=12)
plt.tight_layout()
plt.savefig('barplot.png')

Gràfic de línies. Evolució.

Agafem les dades d'un estudi d'Stanford sobre com es coneixen les parelles a través dels anys i mostrem un gràfic de com ha evolucionat als anys 2002, 2012 i 2022.

import pandas as pd
import matplotlib.pyplot as plt

# Basat en l'estudi de 
# https://data.stanford.edu/hcmst2017
data = {
    'Year': [2002, 2012, 2022],
    'Online': [15, 25, 50],
    'Friends': [35, 30, 20],
    'Work': [25, 20, 15],
    'Education': [17, 16, 12],
    'Other': [8, 9, 3]
}
df = pd.DataFrame(data)

# Bucle per generar les línies del gràfic
for column in df.columns[1:]:  # Ometem la primera columna 'Year'
    plt.plot(df['Year'], df[column], marker='o', label=column)

plt.xlabel('Any')
plt.ylabel('Percentatge (%)')
plt.title('Evolució de com es coneixen les parelles (2002-2022)')

# Per algun motiu que desconeixem, estem obligats a convertir Year.
df['Year'] = df['Year'].astype(int)
plt.xticks(df['Year'], rotation=45)

plt.legend()
# plt.tight_layout()
plt.savefig('lineplot.png')

Exercicis: estudi oestoporosi.

Importa aquest estudi sobre la osteoporosis que es va fer a divereses pacients

Tingueu en compte que si no el tenim el descarregui i si ja el tenim només el llegeixi.

Article i fitxer originals:

Què demanem ?

  1. Fes que el dataframe només contingui les següents columnes:
  • edad (en anys).

  • imc (índex de massa corporal)

  • bua (resultat de l'exploració densitomètrica)

  • classific (normal / osteopenia / osteoporosi)

  • menarqui (edat primera menstruació, en anys)

  • edat_menop (edat inici menopausa, en anys)

  • menop → menopausa (sí, no)

  • tipus de menopausa

  1. Obtén els dtypes i el tamany en memòria, per veure si s’ha carregat bé. → info

  2. Mostra les 10 primeres files.

  3. Obtén la mitjana de l'imc i de la edat de les pacients. → describe

  4. Mostra les dades de la pacient més jove de l'estudi

  5. Mira si hi ha valors NaN en alguna columna. En quines?

  6. Obtén la classificació de les pacients que van des de la posició 100 a la 199. → loc, iloc

  7. Obtén la edat i l’imc de les pacients que van des de la posició 50 al 99. → loc, iloc

  8. Transforma dos camp cap al tipus categorical.

  9. Fes el recompte d’ocurrències d’una variable object i una de categorical; ordenades.

  10. Guarda els canvis en un nou fitxer.

Consideracions importants!

  • Assegura't que al carregar el dataFrame s'agafen els tipus adequats, usant el atribut dtype dins de la funció read_csv.

  • Ves en compte amb l'IMC i altres tipus de dades, que separen el decimal amb la , i no pas amb el . com espera pandas per defecte. Ho has de convertir correctament.

import pandas as pd
import os
from urllib.request import urlretrieve

url = "https://gitlab.com/xtec/bio/pandas/-/raw/main/data/osteoporosis.csv"
csv_file = "oestoporisi.csv"
csv_new_file = "nou_oestoporisi.csv"

# Descarrega el fitxer si no existeix
if not os.path.isfile(csv_file):
    urlretrieve(url, csv_file)

df_oestop = pd.read_csv(csv_file, sep="\t", decimal=',', dtype={
    'edad': 'int32',
    'bua': 'float64',
    'clasific': 'category',
    'edad_men': 'int32',
    'menop': 'category',
    'tipo_men': 'category',
    'imc': 'float64'
})

# 1. Filtrar el dataframe per les columnes requerides
df_oestop = df_oestop[['edad', 'imc', 'bua', 'clasific', 'edad_men', 'menop', 'tipo_men']]

# 2. Obtenir info del dataframe
print(df_oestop.info())

# 3. Mostrar les 10 primeres files
print(df_oestop.head(10))

# 4. Obtenir la mitjana de l'IMC i de l'edat de les pacients
print(df_oestop[['edad', 'imc']].describe())

# 5. Mostrar les dades de la pacient més jove
# Trobar l'edat mínima
min_edad = df_oestop['edad'].min()

# Filtrar el DataFrame per mostrar les dades de la pacient més jove
pacient_mes_jove = df_oestop[df_oestop['edad'] == min_edad]
# Solució alternativa no vista.
# print(df_oestop.loc[df_oestop['edad'].idxmin()])

# 6. Comprovar si hi ha valors NaN en alguna columna
print(df_oestop.info())

# Solució que no hem vist.
# print(df_oestop.isna().sum())

# 7. Classificació de les pacients de la posició 100 a 199
print(df_oestop.loc[100:199, 'clasific'])

# 8. Edat i IMC de les pacients de la posició 50 al 99
print(df_oestop.loc[50:99, ['edad', 'imc']])

# 9. Transformar dos camps cap al tipus categorical
df_oestop['menop'] = df_oestop['menop'].astype('category')
df_oestop['clasific'] = df_oestop['clasific'].astype('category')

# 10. Recompte d’ocurrències
print(df_oestop['menop'].value_counts().sort_index())
print(df_oestop['clasific'].value_counts().sort_index())

# 11: guardar el nou fitxer si es vol
ruta_nou_fitxer = "nou_fitxer_oestoporosi.csv"
df_oestop.to_csv(ruta_nou_fitxer, sep=";", index=False) 

Altres exemples:

# Dades de pacients entre 60 i 70 anys
pacients_60_70 = df_oestop.loc[(df_oestop['edad'] >= 60) & (df_oestop['edad'] <= 70)]
print(pacients_60_70)
# Filtrar pacients amb osteoporosi
pacients_osteoporosi = df_oestop.loc[df_oestop['clasif'] == 'OSTEOPOROSI']
print(pacients_osteoporosi)

14.- Crea un gràfic del recompte de pacients del camp clasific: quants tenen estat normal, oestoporosi i oestopenia.

Agafa el mateix codi que has usat per crear el dataframe i afegeix aquestes línies per a generar el gràfic.

# Crear el gràfic de barres
plt.figure(figsize=(8, 5))
recompte_clasific.plot(kind='bar', color=['blue', 'orange', 'green'])
plt.title('Recompte de Pacients per Classificació')
plt.ylabel('Nombre de Pacients')
plt.xticks(rotation=65)
plt.grid(axis='y')
plt.tight_layout()
plt.savefig('oesto.png')

Exercicis reforç: loc & iloc i selecció condicional.

Donat aquest codi que crea dades generals sobre alumnes, crea un dataframe i realitza les següents operacions.

student_list=["John","Mary","Lucy","Peter","Ann","Tom", "Oliver", "Luna", "Aria"]
grades_list = [7,9,8,4,10,6,np.nan,np.nan,np.nan]
start_date = pd.date_range("20210101", periods=9)
genders = pd.Categorical(["Male", "Female", "Non-Binary", "Male", "Female", "I prefer not to say", "Male", "I prefer not to say", "Female"])

datos: dict[list] = {
      "grade": grades_list,
      "student_list" : student_list,
      "start_date" : start_date,
      "gender" : genders}

exercicis_frame = pd.DataFrame(
    index=student_list,
    data=datos
)

Operacions.

  1. L'index ha de ser el nom de l'alumne. Apart de ser índex també ha de ser un camp.
  2. Mostra la mitjana de notes de tots els alumnes.
  3. Ordena els alumnes alfabèticament.
  4. Mostra tota la info d'un alumne, a partir del seu nom.
  5. Mostra les notes dels 3 alumnes que tenen una nota més alta.
  6. Mostra els alumnes que tenen una nota superior o igual a 7.
  7. Espai per a que creis una consultes i la seva solució, a partir de les noves consultes que has creat.
  8. Crea un camp de gènere categòric.

Ja ho hem fet al crear el dataframe. Si no ho haguessim fet hauriem d’usar el mètode set_index

student_list=["John","Mary","Lucy","Peter","Ann","Tom", "Oliver", "Luna", "Aria"]
grades_list = [7,9,8,4,10,6,np.nan,np.nan,np.nan]
start_date = pd.date_range("20210101", periods=9)
genders = pd.Categorical(["Male", "Female", "Non-Binary", "Male", "Female", "I prefer not to say", "Male", "I prefer not to say", "Female"])

datos: dict[list] = {
      "grade": grades_list,
      "student_list" : student_list,
      "start_date" : start_date,
      "gender" : genders}

#1. L'index ha de ser el nom els estudiants.
exercicis_frame = pd.DataFrame(
    index=student_list,
    data=datos
)

#2. Mostra la mitjana de notes de tots els alumnes.
print(f"Mitjana de les notes: {exercicis_frame["grade"].mean()}")

#3. Ordena els alumnes alfabèticament.
exercicis_frame3= exercicis_frame.sort_values(by=["student_list"],ascending=True)
print(exercicis_frame3)

# 4. Mostra tota la info d'un alumne, a partir del seu nom.

print("EX4 - Info d'una alumne")
print(exercicis_frame.loc["Mary"])

# Si volguessim info sobre només un atribut d'un alumne.
# students_frame.loc[["Mary"],"grade"]

# 5. Mostra les notes dels 3 alumnes que tenen una nota més alta.
#Possible solució - ordenar els alumnes per nota i mostrar els 3 primers.
exercicis_frame5 = exercicis_frame.sort_values(by=["grade"],axis=0,ascending=False)
print(exercicis_frame6[0:3])

# 6. Alumnes amb nota superior o igual a 7
alumnes_nota_7_o_mes = exercicis_frame[exercicis_frame['grade'] >= 7]
print("Alumnes amb nota superior o igual a 7:")
print(alumnes_nota_7_o_mes)

# 7. Consulta que tu vulguis. Exemples. 
print("Consulta tota la info dels alumnes Ann, Lucy i John.")
print(exercicis_frame.loc[["Ann","Lucy","John"]])

# Consulta les notes de la Mary.
print(exercicis_frame.loc["Mary","grade"])

# Mostra el camp index i la nota dels alumnes, ordenats alfabèticament.
print("Mostra el camp index i la nota dels alumnes que tenen nota (no tots tenen).")
exercicis_frame11 = exercicis_frame.loc[exercicis_frame['grade'] > 0]
print(exercicis_frame.loc[exercicis_frame11.index,'grade'])

# 8. Crea un camp de gènere categòric.
# Si no ho haguessim fet en la creació, ho fariem així:
print("Categories de gènere:")
print(exercicis_frame['gender'].cat.categories)

Agrupació i agregació, molt important per a l'anàlisi de dades

En realitzar l'agregació, transformem les nostres dades en informació.

Per exemple, ens pot interessar agrupar tots els pacients per grups d'edat, o per gènere; per a fer un recompte de quants n'hi ha de cada (pex 11 dones i 14 homes).

També, per fer altres funcions d'agregació un cop agrupades; com la mitjana del sou d'homes i dones.

Exemple: Conjunt de Dades dels Pingüins de Palmer

En aquest tema, utilitzarem el conjunt de dades dels Pingüins de Palmer com a exemple.

Pots importar-lo des de GitHub amb les següents línies:

import pandas as pd
from urllib.request import urlretrieve
import os.path

csv_file : str = 'penguins.csv'
url : str = 'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv'

if not os.path.isfile(csv_file):
   print(f"Descarregant fitxer {csv_file}")
   urlretrieve(url,csv_file)

df = pd.read_csv(url)

El títol de cada columna parla per si mateix:

df.head(3)

Sortida:

  species    island  bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g  sex
0  Adelie  Torgersen          39.1          18.7            181.0       3750.0    MALE
1  Adelie  Torgersen          39.5          17.4            186.0       3800.0  FEMALE
2  Adelie  Torgersen          40.3          18.0            195.0       3250.0  FEMALE

Observació: bill_length_mm i bill_depth_mm són la longitud i profunditat del bec (on mengen) i flipper_lenght_mm la longitud de les aletes.

DataFrame.aggregate

.aggregate() és un mètode de pandas DataFrame i Series que s'utilitza per a l'agregació de dades. Es pot utilitzar per a una o moltes funcions al llarg de qualsevol eix. Per estalviar temps per a coses realment importants (com el Machine Learning o Netflix), potser voldràs utilitzar la versió més curta del mètode — .agg(). Vegem els següents exemples.

Suposem que volem trobar el valor mitjà de la massa corporal del pingüí. Ho pots fer aplicant el mètode .agg() a la sèrie desitjada:

df.body_mass_g.agg('median')

Sortida:

4050.0

Quatre quilos de tendresa antàrtica en blanc i negre!

Consell: Pots passar una funció, el nom de la funció (com a cadena de text), una llista de funcions (o els seus noms) o un diccionari a .agg().

Per exemple, 'unique' en df.body_mass_g.agg('unique') obtindrà els elements únics de la columna body_mass_g, 'nunique' comptarà els elements únics en la columna especificada, i 'sum' en df.body_mass_g.agg('sum') produirà la suma dels valors de body_mass_g.

A més, pots passar una funció així (aquí passem la funció incorporada de Python sum(), però es pot passar qualsevol funció que gestioni un DataFrame):

df.body_mass_g.agg(sum)

Una altra manera de fer-ho és cridant al mètode .median() de la sèrie:

df.body_mass_g.median()

La diferència és que utilitzant el mètode .agg(), també podem aplicar diverses funcions d'agregació a diferents columnes. Per exemple, trobem el bec més curt i el mitjà del pingüí, així com les aletes més llargues i les mitjanes.

Per fer això, hem de cridar la funció agg() i crear un diccionari de Python utilitzant les columnes com a claus del diccionari. A més, necessitem posar les funcions d'agregació en llistes:

df.agg({'bill_length_mm': ['min', 'mean'],
         'flipper_length_mm': ['max', 'mean']
        })

Sortida:

  bill_length_mm  flipper_length_mm
 min     32.10000                NaN
mean     43.92193          200.915205
 max           NaN          231.000000

Recompte de valors NaN.

Consell: La funció retorna NaN per als valors que no hem especificat.

També és possible agregar les dades amb les teves pròpies funcions. L'exemple de la funció a continuació retorna el nombre de valors perduts. Si el conjunt no conté aquests valors, posa 0 (per defecte):

def count_nulls(series, ok_message=0):
    if not series.isna().sum():
        return ok_message
    return len(series) - series.count()

Fem-ho servir en el nostre DataFrame:

df.agg(count_nulls)

Sortida:

species               0
island                0
bill_length_mm        2
bill_depth_mm         2
flipper_length_mm     2
body_mass_g           2
sex                  11
dtype: int64

Si necessites passar un paràmetre a la funció agg(), simplement enumera els seus noms i valors separats per una coma després del nom de la funció:

df.agg(count_nulls, ok_message='Hurray!')

Sortida:

species              Hurray!
island               Hurray!
bill_length_mm             2
bill_depth_mm              2
flipper_length_mm          2
body_mass_g                2
sex                       11
dtype: object

Consell: Si utilitzes funcions no pandas (NumPy o les teves pròpies) — no posis els seus noms entre cometes.

Si vols trobar el valor més gran d'un conjunt específic de característiques per a cada pingüí (suposem que volem veure el valor més gran de 3 característiques — bill_length_mm, bill_depth_mm, i flipper_length_mm), aplica .agg() sobre les columnes establint l'argument axis a 'columns'.

També podem trucar al mètode .agg() per treballar amb un dataframe que només conté valors numèrics. Utilitzem la funció incorporada de Python max(). Resulta en una sèrie de pandas:

df[['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm']].agg(max, 
axis='columns')

Sortida:

0      181.0
1      186.0
2      195.0
3        NaN
4      193.0
...  
339      NaN
340    215.0
341    222.0
342    212.0
343    213.0
Length: 344, dtype: float64

DataFrame.groupby

Podem obtenir una sortida estadística de diferents columnes amb l'agregació, però per entendre les dades en profunditat, necessitem tenir una mirada més propera a diverses parts i combinacions de columnes. Per a aquest propòsit, podem utilitzar groupby(). És una eina molt senzilla, especialment per a aquells que ja estan familiaritzats amb SQL.

Ara comprovem la longitud mitjana del bec per a femelles i mascles. Agrupa tots els pingüins pel seu sexe i agrega'ls amb una línia:

df.groupby(['sex']).agg({'bill_length_mm':'median'})

Sortida:

       bill_length_mm
   sex   
FEMALE           42.8
  MALE           46.8

El df.groupby(['sex']) solament retorna alguna cosa com <pandas.core.groupby.generic.DataFrameGroupBy object at 0x000002A1E7BE8358>. Això és perquè només és un objecte en memòria amb valors dividits per a grups indicats. L'intèrpret no sap què mostrar tret que ho especifiquis directament amb .agg().

Com has notat, la columna "sex" conté 11 valors perduts (alguns pingüins han preferit no compartir el seu gènere). Per incloure'ls en la nostra agrupació, estableix l'argument dropna de groupby a False (es requereix pandas 1.1 o superior):

df.groupby('sex', dropna=False).agg({'bill_length_mm':'median'})

Sortida:

       bill_length_mm
   sex   
FEMALE           42.8
 

 MALE           46.8
   NaN           42.0

Ara podem assumir que els pingüins de sexe desconegut són majoritàriament femelles.

A continuació, suposem que la dieta del pingüí varia per illa, i afecta la longitud del bec. Per comprovar si té sentit o no, agrupem els nostres amics antàrtics per illa i sexe. Tingues en compte que quan necessitem passar diversos arguments, hem de passar una llista:

df.groupby(['island', 'sex']).agg({'bill_length_mm':'median'})

Sortida:

                  bill_length_mm
island    sex                   
Biscoe    FEMALE            44.9
          MALE              48.5
Dream     FEMALE            42.5
          MALE              49.1
Torgersen FEMALE            37.6
          MALE              41.1

L'ordre dels arguments en la llista d'arguments de .groupby() determina l'ordre d'agrupació. Primer vénen els grups, després els subgrups, els subgrups dels subgrups, i així successivament.

Consell: Com has vist fins ara, groupby farà que les etiquetes de grup que li passes es converteixin en columnes d'índex — el paràmetre as_index és responsable d'això i està configurat a True per defecte. Per al nostre últim exemple, si truquem a .index.names, podem veure que tenim 2 índexs — 'island' i 'sex':

df.groupby(['island', 'sex']).agg({'bill_length_mm':'median'}).index.names

La sortida tindrà aquest aspecte: FrozenList(['island', 'sex'])

En cas que vulguis evitar establir l'índex (els índexs) a les etiquetes de grup passades a .groupby(), has d'establir as_index a False:

df.groupby(['island', 'sex'], as_index = False).agg({'bill_length_mm':'median'})

Exercicis agrupació dades dels pingüins.

Des del mateix dataset dels pingüins de Palmer, realitza les segúents operacions.

1.- Retorna el nom de les espècies de pingüins agrupades per illes.

Resultat esperat:

                  species
island                     
Biscoe     [Gentoo, Adelie]
Dream           [Chinstrap]
Torgersen          [Adelie]

2.- Compta el número de pingüins que hi ha a cada illa.

Resultat esperat:

      island  species
0     Biscoe      168
1      Dream      124
2  Torgersen       52

3.- The meanest penguin Mostra el pes mitja en quilograms (mean). Arrodoneix la resposta a 2 decimals.

4.- Compta el número de pingüins femella i mascle dins del dataset.

Resultat esperat aproximat:

       species
sex            
FEMALE       X
MALE         Y

5.- Cadena de pingüins en cada illa Mostra la suma de la longitud de les aletes de cada espècie de pingüí de cada illa, així sabrem quanta distancia abarquen tots els pingüins junts.

                     flipper_length_mm
island    species                     
Biscoe    Adelie                 203.0
          Gentoo                1962.0
Dream     Adelie                 559.0
          Chinstrap              398.0
Torgersen Adelie                 936.0

Recorda com carregar el fitxer:

import pandas as pd

df = pd.read_csv(
'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv')

#print(df.describe())
print(df.head())
print(df.groupby('island', as_index=False).agg({'species': 'count'}))
print(df.groupby('island', as_index=False).agg({'species': 'count'}))
df['body_mass_g'] = df['body_mass_g'] / 1000
print(round(df['body_mass_g'].mean(),2))
print(df.groupby('sex').count())
comp_results = df.groupby(["island","species"]).agg({'flipper_length_mm': 'sum'})

Exemple 2 - Visites eCommerce.

Ja vam veure una funció d’agrupació senzilla i potent: el value_counts, per fer el recompte d’ocurrències dels possibles valors d’un camp.

Però a vegades no és suficient, ens interessa filar més prim. Per exemple, ens interessa sumar valors per gurps d’una categoria concreta, calcular-ne la mitjana, màxim…

agrupació (groupby).

Imaginem-nos que hem muntat un e-Commerce (una botiga digital) i tenim dades de 4 usuaris amb:

  • la data en què van entrar
  • el nom
  • el temps de sessió en minuts (quant de temps van estar a la botiga)
  • el que van gastar quan van comprar.

Els tenim organitzats en aquest dataframe:

df = pd.DataFrame({
    'dia': ['2023-02-22', '2023-02-22', '2023-02-22', '2023-02-22', '2023-02-23', '2023-02-23', '2023-02-23', '2023-02-23'],
    'nom': ['Juan', 'Pepe', 'Carla', 'Cristina', 'Juan', 'Pepe', 'Carla', 'Cristina'],
    'temps_sessio': [10, 32, 7, 15, 2, 5, 6, 7],
    'despesa_total': [20, 39.99, 56.78, 14.24, 14.99, 25.67, 35.17, 8.95]
})
print(df.head(8))

Ja sabem com fer la suma del temps de sessió de tots els usuaris, de fer el recompte d’usuaris per dia i altres.

Però com aconseguim saber el temps de sessió i despesa total de cada dia ?

Molt fàcil: agrupem les dades pel camp dia i fem la funció d’agregació sum

df_suma_temps_despesa_dia = df.groupby('dia').sum()
df_suma_temps_despesa_dia.drop(['nom'],axis=1,inplace=True)
print(df_suma_temps_despesa_dia)

També podem mostrar la despesa total de cada usuari en tots els dies.

print(df.groupby('nom')['despesa_total'].sum().sort_values(ascending=False))

Fixeu-vos com n’és d’interessant fer un gràfic d’aquesta informació.

agregació de files calculades (agg).

Una altra funcionalitat molt potent és crear un dataframe amb diverses dades agrupades de les despeses de cada dia, o per nom.

df_agg = df.groupby('dia').agg(
    total=('despesa_total', sum),
    media=('despesa_total', np.mean))
print(df_agg)
             		total    		media
dia                        
2023-02-22  	131.01  	32.7525
2023-02-23  	114.78  	28.6950

Pregunta: Com ho farem per saber el total, la mitjana i el màxim que ha gastat cada usuari ?

Resposta:

df_agg2 = df.groupby('nom').agg(
    total=('despesa_total', sum),
    mitjana=('despesa_total', np.mean),
    max_dia=('despesa_total', max))
print(df_agg2)
      	total  mitjana  max_dia
nom                         	 
Carla 	91.95   45.975	56.78
Cristina  23.19   11.595	14.24
Josep 	29.00   29.000	29.00
Juan  	34.99   17.495	20.00
Pepe  	65.66   32.830	39.99

Font de l'Exemple e-Commerce

Conclusions agg i groupby

En aquest tema, has après a:

  • Calcular les estadístiques resum de la columna amb l'ajuda de .agg()
  • Separar el conjunt de dades en grups amb l'ajuda de .groupby()

Aquests mètodes de pandas poden ajudar-te a trobar valors atípics en les dades, avaluar una sèrie d'observacions i comparar grups amb la característica estadística desitjada.

No obstant això, tingues en compte el possible resultat quan realitzis una agregació i agrupació complicada de dades reals. Un diagrama dibuixat en paper pot estalviar hores de correcció de codi. Si continues obtenint errors i respostes incorrectes, probablement no tingui res a veure amb l'ús de Python — potser la teva lògica estigui fallant.



Creuament de dades amb DataFrames

Com ja sabeu, hi ha dues estructures de dades principals pandas: Series (unidimensional) i DataFrames (bidimensional).

Abans, vas aprendre a crear objectes dels dos tipus. També heu après que podeu transferir dades de diferents fitxers i taules a DataFrames.

Ara, imagineu-vos que heu de treballar amb diversos conjunts de dades similars o relacionats i processar-los de la mateixa manera.

Què és el primer que podríeu fer per facilitar aquesta tasca? Potser us agradaria combinar-los.

Per sort per a tu, pandas proposa diverses maneres de fer-ho. En aquest tema, aprendràs a unir-te DataFrame i Series objectes utilitzant funcions concat() i merge().

Referència:

  • [https://medium.com/analytics-vidhya/python-tip-6-pandas-merge-dev-skrol-bf0be41f29b7]

Concaternació (concat)

La funció concat s'utilitza per concatenar o enganxar diversos objectes junts al llarg d'un eix horitzontal o vertical.

Per fer-ho, hem de passar múltiples Series o DataFrames com a arguments de la funció.

Però primer, crearem dos DataFrame: les taules que emmagatzemen els noms dels estudiants i els seus resultats després d'una competició d'atletisme en proves de 100 metres i 2 quilòmetres respectivament:

junior_class = pd.DataFrame({'Name': ['Ann', 'Kate', 'George', 'Eric'],
'100m (sec.)': ['16.3', '17.1', '14.8', '14.3'],
'2km (min., sec.)': ['9,24', '9,45', '9,17', '8,14']},
index=[1, 2, 3, 4])

senior_class = pd.DataFrame({'Name': ['Jack', 'Alicia', 'Ella', 'James'],
'100m (sec.)': ['15.9', '17.8', '17.0', '15.0'],
'2km (min., sec.)': ['8,18', '9,02', '8,58', '7,58']})

Tingueu en compte que els seus índexs són diferents i independents entre si: el primer DataFrame està indexat en el rang 1-4 i el segon té el valor predeterminat 0, 1, 2, ... .

Es fa amb finalitats il·lustratives: la indexació és important en la concatenació, així que mostrarem com aquestes diferències afecten els resultats i com es poden restablir els valors inicials.

Ara, hauríem de passar el DataFrames a concat() com a seqüència o mapeig:

pd.concat([junior_class, senior_class])
Name 100m (sec.) 2km (min., sec.)
1 Ann 16.3 9,24
2 Kate 17.1 9,45
3 George 14.8 9,17
4 Eric 14.3 8,14
0 Jack 15.9 8,18
1 Alicia 17.8 9,02
2 Ella 17.0 8,58
3 James 15.0 7,58

Com heu pogut notar, el segon DataFrame s'ha afegit a sobre del primer.

Què passa si els vols afegir un al costat de l'altre?

Tot i que té sentit quan es tracta d'índexs significatius (per exemple, anys, dni, cognoms), en el nostre cas probablement ens agradaria tornar-los a calcular.

Aquests i alguns altres problemes es poden resoldre ajustant els paràmetres següents:

axis — l'eix al llarg del qual s'ha de concatenar. Els valors possibles són 0 i 1 ('0' per defecte): axis=0 significa combinar al llarg de les files, i axis=1 és per combinar al llarg de columnes. Ara establim el valor de axis a 1:

pd.concat([junior_class, senior_class], axis=1)
Name 100m (sec.) 2km (min., sec.) Name 100m (sec.) 2km (min., sec.)
0 NaN NaN NaN Jack 15.9 8,18
1 Ann 16.3 9,24 Alicia 17.8 9,02
2 Kate 17.1 9,45 Ella 17.0 8,58
3 George 14.8 9,17 James 15.0 7,58
4 Eric 14.3 8,14 NaN NaN NaN

Us heu adonat que alguns camps estan plens de valors de NaN?

NaN significa "No és un número".

Aquesta és la manera de gestionar els valors que falten pandas i a Numpy; ho hem vist a:

Valors NaN a Numpy

Com ja hem vist, junior_class i senior_class estan indexats de manera diferent.

Per tant, cada vegada que hi hagi aquests desajustos, els camps de dades no coincidents s'ompliran de NaN.

ignore_index— mantenir o restablir els índexs originals en concatenar. Els valors possibles són "True" i "False" (per defecte "False").

Si voleu que el vostre objecte resultant es torni a ordenar, especifiqueu-lo ignore_index=True.

Ara, l'eix s'etiquetarà amb índexs numèrics que comencen per 0:

pd.concat([junior_class, senior_class], ignore_index=True)
Name 100m (sec.) 2km (min., sec.)
0 Ann 16.3 9,24
1 Kate 17.1 9,45
2 George 14.8 9,17
3 Eric 14.3 8,14
4 Jack 15.9 8,18
5 Alicia 17.8 9,02
6 Ella 17.0 8,58
7 James 15.0 7,58

Si li afegim un tercer dataframe també ens el fa bé:

veteran_class = pd.DataFrame({'Name': ['Mike', 'Lisa'],
'100m (sec.)': ['21.9', '18.8'],
'2km (min., sec.)': ['11,18', '10,02']})
concat_pd = pd.concat([junior_class, senior_class, veteran_class], ignore_index=True)

join — combinant-se amb el ' exterior ' o ' interior tipus d'unió ' ('exterior' per defecte). El tipus d'unió exterior retorna la unió de tots els objectes, és a dir, es conservaran totes les seves files originals. Per contra, el tipus intern inclou només les files etiquetades amb índexs presents als dos conjunts de dades, excloent totes les altres files.

Mireu l'exemple següent: hi ha files marcades amb els números 1, 2 i 3, però les files marcades amb 0 (de senior_class) i 4 (des de junior_class) s'eliminen:

pd.concat([junior_class, senior_class], axis=1, join='inner')
Name 100m (sec.) 2km (min., sec.) Name 100m (sec.) 2km (min., sec.)
1 Ann 16.3 9,24 Alicia 17.8 9,02
2 Kate 17.1 9,45 Ella 17.0 8,58
3 George 14.8 9,17 James 15.0 7,58

keys— afegir un nou nivell d'etiquetes per indicar, per exemple, de quines Series o DataFrame els valors provenen o els agrupa d'una altra manera.

Els noms s'han de passar com una seqüència, per exemple, llista:

pd.concat([junior_class, senior_class], keys=['Jun. class', 'Sen. class'])
Name 100m (sec.) 2km (min., sec.)
Jun. class 1 Ann 16.3 9,24
2 Kate 17.1 9,45
3 George 14.8 9,17
4 Eric 14.3 8,14
Sen. class 0 Jack 15.9 8,18
1 Alicia 17.8 9,02
2 Ella 17.0 8,58
3 James 15.0 7,58

Fusió (merge)

Comparat amb concat(), merge()és una eina de combinació més flexible que ofereix possibilitats d'aprofundir una mica més en l'estructura dels objectes. La funció està arrelada en la idea de l'anomenada unió a l'estil de base de dades : unir-se a partir de columnes compartides.

Si esteu familiaritzat amb els tipus d'unions SQL i SQL, la funcionalitat principal de merge() us recordarà les operacions següents: INNER JOIN, LEFT OUTER JOIN, RIGHT OUTER JOIN i FULL OUTER JOIN.

TODO "pd_join.png" (no està imatge)

La funció necessita dos objectes per operar:

Un DataFrame per una banda; i un altre DataFrame o una Series.

Farem servir el DataFrame cap a junior_class de l'apartat anterior.

Suposem que volem fusionar-lo amb un de nou DataFrame que conté informació sobre l'edat dels participants.

Suposem que inclourem només tres membres al dataframe:

age_of_participants = pd.DataFrame({'Name': ['Ann', 'Eric', 'Ella'],
'Age': ['16', '16', '18']})

Ara, passem els dos DataFrame objectes a merge():

junior_class.merge(age_of_participants)
Name 100m (sec.) 2km (min., sec.) Age
0 Ann 16.3 9,24 16
1 Eric 14.3 8,14 16

Com podeu veure, només tenim dues files. Tots dos junior_class i age_of_participants tenen la mateixa columna "Nom".

Així, per defecte, s'uneixen a partir d'aquesta clau coincident.

Només les files que tenen valors superposats de "Nom" en ambdues DataFrames es produeixen: la informació sobre l'edat dels altres participants no està disponible i tindran NaN contra els seus noms a la columna "Edat", de manera que els seus resultats no s'inclouen a la taula final.

Això passa perquè merge() té un paràmetre semblant a join de concat(), i s'estableix per defecte al tipus d'unió interior. Vegem algunes altres opcions i paràmetres per ajustar els resultats:

how- defineix la manera de fusionar. Els valors possibles són ìnner, outer, left i right per defecte inner.

A dalt, vam veure un exemple de combinació interior; si especifiquem 'exterior', obtindríem la unió de totes les dades.

A veure què passa si escrivim how='left':

junior_class.merge(age_of_participants, how='left')
Name 100m (sec.) 2km (min., sec.) Age
0 Ann 16.3 9,24 16
1 Kate 17.1 9,45 NaN
2 George 14.8 9,17 NaN
3 Eric 14.3 8,14 16

Podem veure totes les entrades del primer conjunt de dades, tot i que alguns valors de la columna "Edat" estan absents. De la mateixa manera, how='right' ens aconseguirà totes les files del segon DataFrame.

Per exemple, la fila, corresponent al nom 'Ella', té valors de NaN en '100m (seg.)' i '2km (min., seg.)', ja que junior_class no conté informació sobre les seves puntuacions:

junior_class.merge(age_of_participants, how='right')
Name 100m (sec.) 2km (min., sec.) Age
0 Ann 16.3 9,24 16
1 Eric 14.3 8,14 16
2 Ella NaN NaN 18

on- permet combinar dos dataframes basant-se en una o més columnes que comparteixen alguna relació.

left_on** i right_on s'utilitzen per especificar les columnes en les quals vols basar la unió dels dos dataframes. La diferència principal entre els dos és:

  • left_on: Especifica la columna (o columnes) del dataframe de l'esquerra (el primer dataframe) que utilitzaràs per fer la unió.
  • right_on: Especifica la columna (o columnes) del dataframe de la dreta (el segon dataframe) que utilitzaràs per fer la unió.

Exemple Pràctic funció merge amb paràmetre on

Imagina que tens dos dataframes: df1 i df2; que volem unir utilitzant les columnes id de df1 i identificador de df2.

import pandas as pd

# Dataframes d'exemple
df1 = pd.DataFrame({
    'id': [1, 2, 3],
    'nom': ['Anna', 'Joan', 'Marta'],
    'edat': [28, 34, 22]
})

df2 = pd.DataFrame({
    'identificador': [1, 2, 4],
    'ciutat': ['Barcelona', 'Madrid', 'València'],
    'país': ['Espanya', 'Espanya', 'Espanya']
})

df_merged = pd.merge(df1, df2, left_on='id', right_on='identificador')

print(df_merged)

Resultat:

| id | nom  | edat | identificador | ciutat    | país    |
|----|------|------|---------------|-----------|---------|
| 1  | Anna | 28   | 1             | Barcelona | Espanya |
| 2  | Joan | 34   | 2             | Madrid    | Espanya |

Amb aquests paràmetres, pots unir dataframes fins i tot si les columnes que utilitzes per unir-los tenen noms diferents en cada dataframe.


Diferències entre concat i merge.

Ara que hem revisat els casos d'ús d'ambdues funcions, aclarim les diferències clau entre elles:

  • Principals casos d'ús. En general, concat() s'utilitza simplement per col·locar diversos objectes un al costat de l'altre o un sobre un altre; al mateix temps, merge()s'utilitza principalment per unir-se com a base de dades: el seu conjunt de paràmetres fa que la unió sigui més flexible i més conscient del contingut.
  • El nombre d'objectes que podem unir. concat() pot acceptar una seqüència de diversos objectes, mentre merge() només ens permet unir-ne dos DataFrames o a DataFrame i un nom Series.
  • Tractament d'eixos (axis). Quan s'utilitza concat(), podeu especificar l'eix al llarg del qual necessiteu unir objectes; merge() només admet la unió una al costat de l'altra.
  • Operacions semblants a bases de dades. concat() només pot realitzar una unió interna o externa, mentre merge() pot fer tipus d'unió interior, exterior, esquerra i dreta.

Exercicis.

Fusiona els 3 dataframe de dades mèdiques amb informació addicional sobre els pacients utilitzant la columna "Pacient" com a clau de fusió. Assegura’t que no apareguin files duplicades i que tota la informació existeix.

import pandas as pd
dades_mediques = {
    'Pacient': ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8'],
    'Pressió Arterial': [130, 118, 125, 140, 122, 118, 135, 128],
    'Temperatura Corporal': [37.1, 36.8, 37.2, 37.5, 37.0, 36.7, 37.3, 37.2]
}
df_mediques = pd.DataFrame(dades_mediques)
informacio_addicional = {
    'Pacient': ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7'],
    'Edat': [35, 42, 28, 54, 46, 31, 57]
}
df_informacio_addicional = pd.DataFrame(informacio_addicional)
informacio_addicional_2 = {
    'Pacient': ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P9'],
    'GrupSang': ['A+', 'A-', 'O+', 'A+', 'B+', 'O-', 'O-', 'A+']
}
df_informacio_addicional_2 = pd.DataFrame(informacio_addicional_2)

Sortida esperada:

Pacient Pressió Arterial Temperatura Corporal Pacient Edat Pacient GrupSang
0 P1 130 37.1 P1 35.0 P1 A+
1 P2 118 36.8 P2 42.0 P2 A-
2 P3 125 37.2 P3 28.0 P3 O+
3 P4 140 37.5 P4 54.0 P4 A+
4 P5 122 37.0 P5 46.0 P5 B+
5 P6 118 36.7 P6 31.0 P6 O-
6 P7 135 37.3 P7 58.0 P8 O-
7 P8 128 37.2 NaN NaN P9 A+

Fusiona els tres DataFrames utilitzant "Pacient" com a clau i amb l'opció "how='outer'" per assegurar que tots els pacients estiguin presents.

df_fusionat = df_mediques.merge(df_informacio_addicional, on='Pacient', how='outer')
df_fusionat = df_fusionat.merge(df_informacio_addicional_2, on='Pacient', how='outer')
print(df_fusionat)

Fixeu-vos que amb el concat no és suficient, per molt que li fiquem outer. Per a què el concat funcionés bé totes les files haurien de ser comunes.

Concaterna els tres DataFrames utilitzant "Pacient" com a clau

df_concat2 = pd.concat([df_mediques,df_informacio_addicional,
df_informacio_addicional_2], axis=1, ignore_index=True, join='outer')
print(df_concat2)

Exemple: fusionar diversos fitxers csv comprimits.

Imagineu-vos que sou un científic de dades i que actualment esteu treballant amb les dades dels hospitals locals.

Tens diversos fitxers amb informació sobre pacients de diferents districtes. De vegades, les dades es divideixen en molts conjunts de dades o poden contenir valors buits o no vàlids.

El primer pas és preprocessar les dades abans de l'anàlisi: combinar els fitxers en un de sol, eliminar files buides o incorrectes, omplir els valors que falten, etc.

En aquesta etapa, tractaràs conjunts de dades que contenen informació sobre pacients de tres hospitals: un general, un prenatal i un altre esportiu.

Aquestes estan en un fitxer zip que conté 3 fitxers csv; anem a resoldre el repte!

import pandas as pd
import os
import urllib3
from zipfile import ZipFile
from io import BytesIO

url: str = "https://gitlab.com/xtec/bio/pandas/-/raw/main/data/hospital.zip"
file_zip: str = "hospital.zip"

if not os.path.isfile(file_zip):
    print("Downloading file")
    http = urllib3.PoolManager()
    resp = http.request(
        "GET", url
    )
    if resp.status == 200:
        with open(file_zip, "wb") as f:
            f.write(resp.data)
    else:
        raise Exception(f"Failed to download file: status code {resp.status}")
   
with ZipFile(file_zip, "r") as zip_ref:
    zip_ref.extractall("data")
        
general = pd.read_csv("data/test/general.csv")
prenatal = pd.read_csv("data/test/prenatal.csv")
sports = pd.read_csv("data/test/sports.csv")

print(general.info())

# print(general.columns)
prenatal = prenatal.rename(columns={"HOSPITAL": "hospital", "Sex": "gender"})
# print(prenatal.columns)
sports = sports.rename(columns={"Hospital": "hospital", "Male/female": "gender"})
# print(sports.columns)

df = pd.concat([general, prenatal, sports], ignore_index=True)

df = df.drop(columns=["Unnamed: 0"])

# Esborrem files que tinguin tot NaN.
df.dropna(axis=0, how="all", inplace=True)

# Ara, substituïm els valors de gender per a què només siguin m o f.
df["gender"].replace(["male", "man", "female", "woman"], ["m", "m", "f", "f"], inplace=True)

# Els Nan de gender de prenatal ens han dit de reemplaçar-los per f.
df.loc[(df["hospital"] == "prenatal") & (df["gender"].isnull()), "gender"] = "f"

# Substituïm valors NaN a les columnes bmi, diagnosis, blood_test, ecg, ultrasound, mri, xray, children, months amb zeros
for x in ["bmi", "diagnosis", "blood_test", "ecg", "ultrasound", "mri", "xray", "children", "months"]:
    df[x].fillna(0, inplace=True)

# La desviació que notem és que la alçada dels pacients de l’hospital d’Sports no s’ha mesurat en cm sinó en peus (1 peu = 30,48 cm aprox)
mask_sports = df['hospital'] == 'sports'
df.loc[mask_sports, 'height'] *= 0.3048

print(df.info())
print(df.sample(n=20, random_state=30))

Resultat:

Index: 1000 entries, 0 to 1005
Data columns (total 14 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   hospital    1000 non-null   object 
 1   gender      1000 non-null   object 
 2   age         1000 non-null   float64
 3   height      1000 non-null   float64
 4   weight      1000 non-null   float64
 5   bmi         1000 non-null   float64
 6   diagnosis   1000 non-null   object 
 7   blood_test  1000 non-null   object 
 8   ecg         1000 non-null   object 
 9   ultrasound  1000 non-null   object 
 10  mri         1000 non-null   object 
 11  xray        1000 non-null   object 
 12  children    1000 non-null   float64
 13  months      1000 non-null   float64
dtypes: float64(6), object(8)
memory usage: 117.2+ KB
None
     hospital gender   age  height  weight   bmi  diagnosis blood_test ecg ultrasound mri xray  children  months
929    sports      f  23.0   6.809   259.0  27.3     sprain          0   f          0   t    f       0.0     0.0
927    sports      m  21.0   6.052   172.0  22.9     sprain          0   f          0   t    f       0.0     0.0
516  prenatal      f  20.0   1.650    66.0  24.3  pregnancy          t   t          t   0    f       1.0     4.0
87    general      m  54.0   1.720    78.0  26.4    stomach          f   0          t   0    0       0.0     0.0
885    sports      f  16.0   5.915   188.0  26.3     sprain          0   f          0   t    f       0.0     0.0
463  prenatal      f  34.0   1.650    67.0  24.9  pregnancy          t   t          t   0    f       1.0     5.0
112   general      m  77.0   1.690    77.0  27.1      heart          t   t          0   0    0       0.0     0.0
297   general      m  56.0   1.480    54.0  25.1       cold          f   0          0   0    0       0.0     0.0
417   general      f  26.0   1.650    69.0  25.3       cold          t   0          0   0    0       0.0     0.0
660  prenatal      f  38.0   1.590    70.0  27.6  pregnancy          t   t          t   0    f       1.0     4.0
344   general      f  60.0   1.410    56.0  28.3    stomach          t   0          f   0    0       0.0     0.0
834    sports      f  21.0   5.585   184.0  29.0   fracture          0   f          0   f    t       0.0     0.0
10    general      m  27.0   1.850    86.0  25.2      heart          t   t          0   0    0       0.0     0.0
56    general      m  23.0   1.650    66.0  24.1      heart          f   t          0   0    0       0.0     0.0
616  prenatal      f  33.0   1.770    81.0  25.9  pregnancy          t   t          t   0    f       1.0     7.0
479  prenatal      f  35.0   1.810    79.0  24.5  pregnancy          t   t          t   0    f       1.0     8.0
578  prenatal      f  31.0   1.770    85.0  27.1  pregnancy          t   t          t   0    f       1.0     8.0
411   general      m  26.0   1.610    70.0  27.2       cold          t   0          0   0    0       0.0     0.0
521  prenatal      f  30.0   1.740    74.0  24.7       cold          t   t          t   0    f       1.0     3.0
941    sports      f  25.0   6.208   222.0  28.2   fracture          0   f          0   f    t       0.0     0.0

Camps calculats.

Crear noves columnes que necessitem en funció d’altres és molt senzill!

Exemples on ens pot interessar:

  • Tenim un camp edat i ens interessa separar per grups d’edat, per a poder fer recomptes i gràfics.
  • Volem juntar nom i cognoms en un mateix camp.
  • Volem calcular l'edat a partir de la data de naixement.
  • Volem calcular l’imc a partir de l’alçada i el pes
  • Volem extreure informació d’un camp string que conté informació rellevant; per exemple ens interessa el primer digit del codi dels ous.

Com crear un nou camp:

Facilissim, si usem el següent dataframe d’exemple:

import pandas as pd
people = {
"first_name": ["Michael", "Michael", 'Jane', 'John'],
"last_name": ["Jackson", "Jordan", 'Doe', 'Doe'],
"birthday": ["29.09.1958", "17.02.1963", "15.03.1978", "12.05.1979"],
"height": [1.75, 1.98, 1.64, 1.8]
}
df = pd.DataFrame(people)

Podem aconseguir un nou camp que es digui full_name amb el cognom i el nom; de la següent manera:

df['full_name'] = df['last_name'] + ', ' + df['first_name']

Sortida:

first_name last_name birthday height full_name
0 Michael Jackson 29.09.1958 1.75 Jackson, Michael
1 Michael Jordan 17.02.1963 1.98 Jordan, Michael
2 Jane Doe 15.03.1978 1.64 Doe, Jane
3 John Doe 12.05.1979 1.80 Doe, John

O bé aconseguir un camp birth_year a partir del birthday:

import datetime
year = datetime.datetime.today().year
df['birth_date'] = df['birthday']
df.head()

Crear un nou camp calculat amb apply:

Això ho aconseguim amb la funció apply. A aquesta li podem passar una funció lambda o una funció pura per a què la apliqui en cada fila (com feiem amb la funció map de Python).

Provem un parell d’exemples amb dades de pacients generades aleatòriament:

import numpy as np
import pandas as pd
import copy

min_value_edat: int = 20
max_value_edat: int = 120
min_value_altura = 1.20
max_value_altura = 2.20
min_value_pes: int = 40
max_value_pes: int = 140

df_pacients = pd.DataFrame({
'edat' : np.random.randint(min_value_edat, max_value_edat ,100),
'altura' : np.random.uniform(min_value_altura, max_value_altura , size=100),
'pes' : np.random.randint(min_value_pes, max_value_pes, size=100)
})

print(df_pacients.head())

Exemple 1. Nou camp imc (bim).

El calculem amb la formula

pes (kgs.) / altura (cms.) ^2

def calcula_imc(df_pacients) -> float:
    return round(df_pacients['pes'] / df_pacients['altura'] ** 2, 4)

axis=1, apliquem resultats a les columnes.

df_pacients['imc'] = df_pacients.apply(calcula_imc, axis=1)

Exemple 2. Nou camp que defineix si un pacient és major a 65 o menor.

def equal_or_over_65_years(age: int) -> bool:
    return "Grup65oMes" if age >= 65 else "GrupMenor65"
    df_pacients['grup_edat'] = df_pacients['edat'].apply(equal_or_over_65_years)
    print(df_pacients.head())

Amb funcions lambda:

df_pacients['grup_edat'] = df_pacients['edat'].apply(lambda age: "Grup65oMes" if age >= 65 else "GrupMenor65")

Sortida:

Fixeu-vos que ha creat correctament els 2 camps.

edat altura pes imc grup_edat
0 95 1.448017 58 27.6618 Grup65oMes
1 20 1.696707 115 39.9470 GrupMenor65
2 115 1.904625 71 19.5722 Grup65oMes
3 63 2.069863 125 29.1761 GrupMenor65
4 50 1.788875 119 37.1867 GrupMenor65

Exemple 3. Classificació de grups d’edat més avançada:

import pandas as pd
import numpy as np
data = {
'Edat': [25, 42, 18, 60, 35, np.nan, np.nan, np.nan],
'Gènere': ['Home', 'Dona', 'Home', 'Dona', 'Home', 'Dona', 'Home', 'Dona']
}
df = pd.DataFrame(data)

Solució

def clasificar_grup_edat(edat):
    if pd.isna(edat):
        return 'Edat desconeguda'
    elif edat < 20:
        return '0-19'
    elif 20 <= edat < 30:
        return '20-29'
    elif 30 <= edat < 40:
        return '30-39'
    elif 40 <= edat < 50:
        return '40-49'
    elif edat >= 50:
        return '50+'
df['grup_edat'] = df['Edat'].apply(clasificar_grup_edat)
print(df)

Sortida:

Edat Gènere Grup d'Edat
0 25.0 Home 20-29
1 42.0 Dona 40-49
2 18.0 Home 0-19
3 60.0 Dona 50+
4 35.0 Home 30-39
5 NaN Dona Edat desconeguda
6 NaN Home Edat desconeguda
7 NaN Dona Edat desconeguda

Si tenim molts intervals d’edat val la pena usar la funció cut i llistes:

Defineix els intervals d'edat i les etiquetes dels grups

bins = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
labels = ['0-10', '11-20', '21-30', '31-40', '41-50', '51-60', '61-70', '71-80', '81-90', '91-100']

Utilitza la funció 'cut' per classificar les edats als grups:

df['grup_edat'] = pd.cut(df['Edat'], bins=bins, labels=labels, right=False)

Codi de l’exemple 3 (les 2 funcions):

import pandas as pd
data = {
    'Edat': [25, 42, 18, 60, 35, 48, 64, 81, 28, 53, 46],
    'Gènere': ['Home', 'Dona', 'Home', 'Dona', 'Home', 'Dona', 'Home', 'Dona', 'Home', 'Dona', 'Home']
}
df = pd.DataFrame(data)

#Defineix els intervals d'edat i les etiquetes dels grups
bins = [0, 10, 20, 30, 40, 50, 60, 70, 80, 100]
labels = ['0-10', '11-20', '21-30', '31-40', '41-50', '51-60', '61-70', '71-80', '80+']

# Utilitza la funció 'cut' per classificar les edats als grups
df['Grup Edat'] = pd.cut(df['Edat'], bins=bins, labels=labels, right=False)
print(df)

Podem constatar que s'ha realitzat la agrupació correctament:

    Edat Gènere Grup Edat
0     25   Home     21-30
1     42   Dona     41-50
2     18   Home     11-20
3     60   Dona     61-70
4     35   Home     31-40
5     48   Dona     41-50
6     64   Home     61-70
7     81   Dona       80+
8     28   Home     21-30
9     53   Dona     51-60
10    46   Home     41-50

Despres d'agrupar les dades, podem obtenir informació valuosa:

# Número de pacients per gènere.
print(df.groupby('Gènere').count())
print(df.groupby('Gènere')['Edat'].mean())

Exemple 4. Agafar una data i extreure’n informació:

Fixeu-vos en aquest exemple, en el que a partir d’una data de naixement agafem només l’any i després calculem la data.

Fixeu-vos que cal garantir que la data sigui format datetime.

import datetime as dt
data = {
'DataNaixement': ['1990-05-15', '1985-10-20', '2000-03-08', '1978-12-30', '1995-07-04'],
'Gènere': ['Home', 'Dona', 'Home', 'Dona', 'Home']
}

df = pd.DataFrame(data)

tipus datetime
df['DataNaixement'] = pd.to_datetime(df['DataNaixement'])
df['AnyNaixement'] = df['DataNaixement'].dt.year
año_actual = dt.datetime.now().year
df['EdatActual'] = año_actual - df['AnyNaixement']
print(df)

Sortida:

DataNaixement Gènere AnyNaixement EdatActual
0 1990-05-15 Home 1990 33
1 1985-10-20 Dona 1985 38
2 2000-03-08 Home 2000 23
3 1978-12-30 Dona 1978 45
4 1995-07-04 Home 1995 28

També funciona per formats de data com el nostre (dd/mm/yyyy)

data2 = {
'DataNaixement': ['15/05/1990', '20/10/1985'],
'Gènere': ['Home', 'Dona']
}
DataNaixement Gènere AnyNaixement EdatActual
0 1990-05-15 Home 1990 33
1 1985-10-20 Dona 1985 38

Projecte REST

1.- En base al projecte https://gitlab.com/xtec/python/pandas que gestiona diversa informació sobre vols al món.

  1. Mostra tots els vols d'un any concret en un endpoint (arrenca l'exemple)

  2. Crea un enpoint que mostri totes les dades d'un registre per index (l'index serà un valor autonumèric).

  3. Crea un o dos endpoints més que filtrin altres dades: ciutat origen/destí, etc...

  4. Crea enpoints RESTful a inserir (POST), actualitzar (PUT) o eliminar (DELETE) un registre del dataframe de vols.

  5. Mostra un gràfic d'algunes columnes dataframe a la web, de dades agrupades.

2.- Crear un servidor d’un Hospital que permeti a aplicacions client consumir les dades de l’hospital tal com s'explica a REST

Crea almenys un mètode tipus GET:

  1. Un que retorni un dataframe de dades públiques d’un hospital en format JSON (no totes les del dataframe)

  2. Si et sobra temps, que mostri un gràfic amb informació pública (això ho veurem a la propera sessió)

  3. A la pròxima sessió també veuràs com posar més funcionalitats.

Pista 1: Aquí teniu com retornar el dataframe del titanic que ve per defecte amb la llibreria Seaborn.

@app.get("/titanic")
def titanic():
   titanic_df = sns.load_dataset("titanic")
   dades_json = titanic_df.to_dict(orient='records')
   return jsonify(dades_json)

Referències