Combinare i DataFrame — impilare con concat, abbinare con merge
Unire le tabelle: impilare mesi degli stessi dati con concat e aggiungere informazioni da un'altra tabella con merge — scegliere il tipo di merge, vedere quali righe non hanno trovato corrispondenza e intercettare la chiave duplicata che moltiplica silenziosamente le righe.
Come vuoi imparare?
Il problema che stiamo risolvendo
I dati del negozio non risiedono in un solo file. Non succede mai.
Gli ordini di marzo sono in orders.csv, la tabella che usi dal quarto capitolo. Gli ordini di aprile sono arrivati questa settimana sotto forma di un'esportazione separata, orders_april.csv. Quanto ciascun prodotto costa al negozio — e quale fornitore lo vende — è gestito dall'ufficio acquisti in un proprio file, products.csv. Nessuno ha inserito il costo nel file degli ordini, perché il costo è una caratteristica del prodotto, non dell'ordine.
Ora la titolare del negozio pone una domanda: quanto profitto ha generato ciascun fornitore tra marzo e aprile?
Nessun singolo file può rispondere. Il profitto richiede la quantità e il prezzo di vendita (negli ordini), il costo (nel file dei prodotti) e il fornitore (anch'esso nel file dei prodotti). E gli ordini sono divisi tra due mesi diversi.
In un foglio di calcolo copieresti le righe di aprile sotto quelle di marzo, poi scriveresti un VLOOKUP in una nuova colonna per recuperare il costo di ciascuna riga, lo trascineresti verso il basso e spereresti in bene. Pandas svolge gli stessi due compiti in due sole righe:
- impilare tabelle che contengono lo stesso tipo di righe — marzo e aprile — in un'unica tabella più lunga:
pd.concat - abbinare ciascuna riga di una tabella alla riga corrispondente di un'altra in base a una colonna condivisa — ciascun ordine al rispettivo prodotto — e importare le colonne dell'altra tabella:
pd.merge
Le righe di codice sono brevi. Ciò che rende importante questo capitolo è quello che può andare storto senza alcun errore: un merge può scartare silenziosamente gli ordini il cui prodotto manca dalla tabella di lookup, oppure duplicare silenziosamente gli ordini quando la tabella di lookup elenca un prodotto due volte. In entrambi i casi si ottiene un totale che sembra del tutto ragionevole ma è errato. Perciò gran parte di questo capitolo è dedicata a sapere, prima e dopo il merge, esattamente quante righe dovresti avere — e a verificarlo.
Alla fine di questo capitolo sarai in grado di
- Distinguere le due operazioni: impilare righe (
concat) e abbinare in base a una chiave (merge) - Impilare tabelle con
pd.concat, correggere l'indice ripetuto conignore_index=Truee vedere cosa succede quando le colonne non corrispondono - Unire due tabelle su una chiave con
pd.merge, scegliendo consapevolmente trahow="inner","left","right"o"outer" - Verificare un merge con
indicator=Truee proteggerlo convalidate="many_to_one" - Riconoscere e risolvere l'esplosione delle righe dovuta a chiavi duplicate
- Eseguire il merge su chiavi con nomi diversi con
left_on/right_one gestire i nomi di colonna in conflitto consuffixes - Riconoscere gli errori del merge — tipi di chiave incompatibili, colonne chiave mancanti — e i fallimenti silenziosi che non generano alcuna eccezione
Prerequisiti: Aggiungere colonne.
Crea prima i file
Questo capitolo usa quattro piccoli file. Mettili accanto al tuo script.
orders.csv — marzo, lo stesso file dei capitoli precedenti:
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0orders_april.csv — aprile, stesse colonne. Nota la cucitrice: il negozio ha iniziato a venderla ad aprile.
order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0products.csv — una riga per prodotto, gestito dall'ufficio acquisti. Due cose sono volutamente "sbagliate" qui, proprio come accadrebbe nella vita reale: la nuova cucitrice non è stata ancora aggiunta e c'è un pennarello che il negozio non ha mai venduto.
product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alphabranches.csv — chi gestisce ciascuna delle tre filiali del negozio. La colonna si chiama branch_name, non branch, perché questo file è stato creato da una persona diversa.
branch_name,manager
north,Mira
south,Omar
east,LenaPrima di scrivere il codice
Combinare tabelle è il passaggio in cui una rapida riga di codice produce più spesso una risposta sbagliata data con sicurezza. Perciò la pianificazione viene prima di tutto, e consiste perlopiù in domande a cui rispondi semplicemente osservando i dati.
1. Esprimi la domanda in una sola frase
"Profitto per fornitore, marzo e aprile insieme." Questa frase ti dice già come sarà la tabella finale: una riga per fornitore, un numero per riga. Esistono due fornitori, Alpha e Bravo, quindi la risposta dovrebbe avere all'incirca due righe. Se finisce per averne tre, o una sola, qualcosa è andato storto lungo il percorso.
2. Decidi quale operazione serve per ciascuna coppia di tabelle
Esistono solo due tipi di combinazione, e il test è semplice: le due tabelle contengono lo stesso tipo di riga, oppure dati diversi relativi alla stessa cosa?
- Stesse colonne, righe diverse — ordini di marzo + ordini di aprile. Usa
pd.concat([a, b]). Il risultato cresce verso il basso: più righe, le stesse colonne. - Una colonna chiave condivisa, dati diversi — ordini + prodotti, collegati da
product. Usapd.merge(a, b, on="product"). Il risultato cresce di lato: le stesse righe, più colonne.
Marzo e aprile sono entrambi "ordini" — stesse colonne, più righe — quindi vengono impilati. Ordini e prodotti sono cose diverse collegate dal nome del prodotto, quindi vengono abbinati.
3. Esamina ogni tabella prima di combinarla
Nel capitolo tre hai imparato a verificare un file non appena viene letto. In presenza di più file questa abitudine non è facoltativa: hai bisogno della dimensione (shape) di ciascuno per prevedere la dimensione del risultato.
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
products = pd.read_csv("products.csv")
for name, df in [("march", march), ("april", april), ("products", products)]:
print(name, df.shape, list(df.columns))march (8, 7) ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
april (4, 7) ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
products (6, 3) ['product', 'cost', 'supplier']Marzo e aprile hanno esattamente le stesse sette colonne, nello stesso ordine — perfette per l'impilamento. products condivide con essi esattamente un nome di colonna: product. Questa è la chiave, la colonna su cui verrà effettuato l'abbinamento.
Verifica che la chiave abbia lo stesso tipo su entrambi i lati. Una chiave che è un numero in una tabella e testo nell'altra non può trovare corrispondenza:
print(march["product"].dtype, products["product"].dtype)str strEntrambe str. Possono essere confrontate.
4. Verifica che la chiave nella tabella di lookup sia univoca
Questo è il controllo più importante dell'intero capitolo. Ciascun ordine ha bisogno di un solo costo. Se products.csv elenca pen due volte, ogni ordine di penne troverà corrispondenza due volte e comparirà due volte nel risultato.
print(products["product"].is_unique)
print(products["product"].duplicated().sum())True
0is_unique è True e ci sono zero duplicati: una riga per prodotto. Ciascun ordine può trovare corrispondenza al massimo con una riga dei prodotti.
5. Controlla quali chiavi non troveranno corrispondenza
Prima di effettuare il merge, chiediti: ci sono ordini il cui prodotto manca da products.csv, e prodotti che nessuno ha mai ordinato? isin (dal capitolo sul filtraggio) risponde a entrambe le domande:
orders = pd.concat([march, april], ignore_index=True)
no_cost = ~orders["product"].isin(products["product"])
print(orders.loc[no_cost, ["order_id", "product"]])
never_sold = ~products["product"].isin(orders["product"])
print(products.loc[never_sold, "product"].tolist())order_id product
10 1011 stapler
['marker']Un ordine — la cucitrice, 1011 — non ha alcun costo registrato. Un prodotto, il pennarello, non è mai stato venduto. Ora sai prima del merge che cosa dovrà gestire l'operazione.
6. Fai una previsione sul risultato, poi decidi
Metti per iscritto la previsione, in numeri:
ordersha 8 + 4 = 12 righe.productsha una riga per prodotto, quindi l'abbinamento non può moltiplicare gli ordini.- Se ogni ordine viene mantenuto, la tabella unita ha 12 righe, e una di esse (la cucitrice) non ha alcun costo.
- Se vengono mantenuti solo gli ordini che trovano corrispondenza, ne ha 11.
Quale delle due opzioni desideri? Per un report sui profitti, scartare silenziosamente l'ordine della cucitrice sottostimerebbe le vendite. È meglio mantenerlo, vedere che il suo costo è mancante e decidere cosa fare — che è esattamente la scelta offerta dall'argomento how=. Dopo il merge, la prima cosa che stampi è la shape, e la confronti con questa previsione.
Questo è l'intero piano: domanda in una sola frase, operazione per ciascuna coppia, esame di ogni tabella, univocità della chiave, chiavi senza corrispondenza, conteggio previsto delle righe. Il resto del capitolo presenta gli strumenti per metterlo in pratica.
Impilare le righe con pd.concat
pd.concat accetta una lista di tabelle e le posiziona l'una sotto l'altra:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
both = pd.concat([march, april])
print(both.shape)
print(both[["order_id", "date", "product", "quantity"]])(12, 7)
order_id date product quantity
0 1001 2024-03-01 pen 12
1 1002 2024-03-01 notebook 5
2 1003 2024-03-02 bag 2
3 1004 2024-03-02 bottle 7
4 1005 2024-03-03 eraser 30
5 1006 2024-03-03 bag 1
6 1007 2024-03-04 pen 20
7 1008 2024-03-04 bottle 4
0 1009 2024-04-01 pen 15
1 1010 2024-04-01 notebook 3
2 1011 2024-04-02 stapler 6
3 1012 2024-04-02 bag 1Dodici righe, le stesse sette colonne. Pandas ha allineato le colonne per nome, non per posizione — se il file di aprile avesse elencato le colonne in un ordine diverso, sarebbero comunque finite sotto le intestazioni corrette.
Guarda però il margine sinistro. L'indice va da 0 a 7, poi ricomincia da 0. concat ha mantenuto l'indice proprio di ciascuna tabella, quindi le etichette da 0 a 3 compaiono ora due volte. Non è un problema puramente estetico:
print(both.loc[0, ["order_id", "date"]])order_id date
0 1001 2024-03-01
0 1009 2024-04-01Hai chiesto la riga 0 e ne hai ottenute due. Qualsiasi codice che dia per scontato che un'etichetta corrisponda a una sola riga — loc, un successivo merge sull'indice, l'aggiornamento di una singola cella — ora fa qualcosa che non intendevi fare. Puoi verificarlo:
print(both.index.is_unique)Falseignore_index=True — rinumera le righe
Quando il vecchio indice era un semplice conteggio di righe (un RangeIndex, come in ogni read_csv visto finora), non ha alcun significato; perciò scartalo e lascia che concat numeri il risultato a partire da zero:
orders = pd.concat([march, april], ignore_index=True)
print(orders.index.is_unique)
print(orders[["order_id", "date", "product"]].tail(5))True
order_id date product
7 1008 2024-03-04 bottle
8 1009 2024-04-01 pen
9 1010 2024-04-01 notebook
10 1011 2024-04-02 stapler
11 1012 2024-04-02 bagOra le etichette vanno da 0 a 11, ciascuna esattamente una volta. Fai di ignore_index=True la tua scelta predefinita quando impili i file; evitalo solo quando l'indice ha un significato (ad esempio dopo set_index("order_id"), dove gli ID degli ordini sono già univoci).
Tieni traccia della provenienza di ciascuna riga
Dopo l'impilamento, nulla nella tabella indica a quale mese appartenga una riga. Qui la data lo rivela, ma spesso non c'è nulla a farlo. Aggiungi una colonna prima di concatenare — la tecnica del capitolo precedente — e l'informazione viaggerà insieme alle righe:
march["month"] = "March"
april["month"] = "April"
orders = pd.concat([march, april], ignore_index=True)
print(orders["month"].value_counts())month
March 8
April 4
Name: count, dtype: int64I conteggi fungono anche da controllo: 8 + 4 = 12, nulla è andato perduto.
Quando le colonne non corrispondono
Concat allinea le colonne per nome. Una colonna che esiste solo in alcune delle tabelle compare comunque nel risultato — e le righe provenienti dalle tabelle che non la possedevano ricevono NaN in quella posizione. Supponiamo che un ordine occasionale sia stato inserito a mano, con una colonna discount e senza date, product, category o price:
manual = pd.DataFrame({
"order_id": [2001],
"branch": ["north"],
"quantity": [3],
"discount": [0.1],
})
mixed = pd.concat([march.head(2), manual], ignore_index=True)
print(mixed[["order_id", "branch", "price", "discount"]])
print(mixed.isna().sum())order_id branch price discount
0 1001 north 15.0 NaN
1 1002 south 60.0 NaN
2 2001 north NaN 0.1
order_id 0
date 1
branch 0
product 1
category 1
quantity 0
price 1
month 1
discount 2
dtype: int64Nessun errore. Il risultato contiene l'unione di tutte le colonne, e i vuoti sono riempiti con NaN. Ecco perché l'impilamento merita lo stesso isna().sum() che hai imparato nel capitolo sui dati mancanti: un nome di colonna digitato diversamente in un file (Quantity rispetto a quantity) non fallisce, ma produce due colonne mezze vuote. Se dopo un concat il numero di colonne è maggiore rispetto a quello di qualsiasi tabella in ingresso, qualcosa non era allineato.
axis=1 — e perché di solito non lo vuoi
pd.concat([a, b], axis=1) affianca invece le tabelle, abbinando le righe per etichetta dell'indice. Si è tentati di usarlo per "aggiungere le colonne dei prodotti agli ordini", ma non guarda affatto al nome del prodotto — la riga 0 di una tabella viene messa accanto alla riga 0 dell'altra, indipendentemente da cosa contengano. Per abbinare in base a una colonna, usa merge.
Abbinare le righe con pd.merge
merge accetta due tabelle e una chiave. Per ogni riga della tabella di sinistra, trova le righe della tabella di destra che hanno lo stesso valore della chiave e ne unisce le colonne:
products = pd.read_csv("products.csv")
merged = pd.merge(orders, products, on="product")
print(merged.shape)
print(merged[["order_id", "product", "quantity", "price", "cost", "supplier"]])(11, 10)
order_id product quantity price cost supplier
0 1001 pen 12 15.0 9.0 Alpha
1 1002 notebook 5 60.0 40.0 Alpha
2 1003 bag 2 850.0 600.0 Bravo
3 1004 bottle 7 120.0 80.0 Bravo
4 1005 eraser 30 8.0 5.0 Alpha
5 1006 bag 1 850.0 600.0 Bravo
6 1007 pen 20 15.0 9.0 Alpha
7 1008 bottle 4 120.0 80.0 Bravo
8 1009 pen 15 15.0 9.0 Alpha
9 1010 notebook 3 60.0 40.0 Alpha
10 1012 bag 1 850.0 600.0 BravoOgni ordine ha ora accanto il rispettivo costo e fornitore, senza alcun ciclo e senza che l'ordine delle righe nei due file abbia importanza. La colonna chiave product compare una sola volta, perché è identica su entrambi i lati.
Ora confronta la shape con il piano. Avevamo previsto 12 righe se tutto fosse stato mantenuto — e ne abbiamo 11. L'ordine della cucitrice, 1011, è sparito. Nessun avviso, nessun errore.
Non si tratta di un bug. È il tipo predefinito di merge, how="inner", che fa ciò per cui è definito: mantenere solo le chiavi presenti in entrambe le tabelle. La cucitrice non è presente in products, quindi il suo ordine non sopravvive; il pennarello non è presente in orders, quindi non compare nemmeno lui. Se non avessi previsto 12 righe, non ti saresti mai accorto della vendita mancante.
I quattro tipi di merge
L'argomento how= decide cosa accade alle righe la cui chiave non trova corrispondenza dall'altro lato. Immagina i due insiemi di chiavi:
how="inner"(il valore predefinito) mantiene solo le chiavi presenti in entrambe le tabelle. Le righe senza corrispondenza su entrambi i lati vengono eliminate. Usalo quando vuoi solo record completi.how="left"mantiene ogni riga della tabella di sinistra. Le righe di sinistra senza corrispondenza ricevonoNaNnelle colonne della tabella di destra; le righe di destra senza corrispondenza vengono eliminate. Usalo per arricchire una tabella principale — la scelta più comune.how="right"è l'immagine speculare: ogni riga della tabella di destra viene mantenuta, mentre le righe di sinistra senza corrispondenza vengono eliminate.how="outer"mantiene ogni chiave da entrambi i lati, riempiendo i vuoti conNaN. Usalo per confrontare e verificare due elenchi tra loro.
La domanda da farsi è: qual è la tabella le cui righe non devono assolutamente sparire? Qui sono gli ordini. Ogni ordine è una vendita reale; il file dei prodotti è una tabella di lookup. Quindi il merge corretto è un left merge, con gli ordini a sinistra:
merged = pd.merge(orders, products, on="product", how="left")
print(merged.shape)
print(merged[["order_id", "product", "cost", "supplier"]])(12, 10)
order_id product cost supplier
0 1001 pen 9.0 Alpha
1 1002 notebook 40.0 Alpha
2 1003 bag 600.0 Bravo
3 1004 bottle 80.0 Bravo
4 1005 eraser 5.0 Alpha
5 1006 bag 600.0 Bravo
6 1007 pen 9.0 Alpha
7 1008 bottle 80.0 Bravo
8 1009 pen 9.0 Alpha
9 1010 notebook 40.0 Alpha
10 1011 stapler NaN NaN
11 1012 bag 600.0 BravoDodici righe, come previsto. L'ordine della cucitrice è ancora presente, con NaN per costo e fornitore — il che è onesto: il costo è sconosciuto, e puoi vedere chiaramente che lo è. Un valore mancante che puoi vedere è di gran lunga preferibile a una riga mancante che non puoi vedere. Da qui in poi si applicano gli strumenti del capitolo sui dati mancanti: puoi contare i vuoti con isna().sum(), inserire il costo se l'ufficio acquisti te lo comunica, oppure segnalare l'ordine separatamente.
Un right merge è l'immagine speculare — ogni prodotto viene mantenuto, e il pennarello non ha alcun ordine:
right = pd.merge(orders, products, on="product", how="right")
print(right.shape)
print(right[["order_id", "product", "cost"]].tail(3))(12, 10)
order_id product cost
9 1008.0 bottle 80.0
10 1005.0 eraser 5.0
11 NaN marker 25.0Nota che order_id è passato da numeri interi a 1008.0, 1005.0 e così via. La riga del pennarello non ha alcun ID ordine, quindi la colonna ora contiene un NaN — e, come hai visto nel capitolo sui dati mancanti, una colonna di interi che riceve un NaN diventa di tipo float64. Se gli ID mostrano improvvisamente un .0 dopo un merge, il motivo è questo: alcune righe non hanno trovato corrispondenza.
indicator=True — scopri da dove proviene ogni riga
Un outer merge mantiene tutto da entrambi i lati. Aggiungendo indicator=True, pandas inserisce una colonna, _merge, che indica per ciascuna riga se la sua chiave è stata trovata in entrambe le tabelle, solo a sinistra o solo a destra:
audit = pd.merge(orders, products, on="product", how="outer", indicator=True)
print(audit[["order_id", "product", "cost", "_merge"]])
print(audit["_merge"].value_counts())order_id product cost _merge
0 1003.0 bag 600.0 both
1 1006.0 bag 600.0 both
2 1012.0 bag 600.0 both
3 1004.0 bottle 80.0 both
4 1008.0 bottle 80.0 both
5 1005.0 eraser 5.0 both
6 NaN marker 25.0 right_only
7 1002.0 notebook 40.0 both
8 1010.0 notebook 40.0 both
9 1001.0 pen 9.0 both
10 1007.0 pen 9.0 both
11 1009.0 pen 9.0 both
12 1011.0 stapler NaN left_only
_merge
both 11
left_only 1
right_only 1
Name: count, dtype: int64Questo è il resoconto dettagliato del merge. Undici righe hanno trovato corrispondenza; un ordine non ha alcuna riga nei prodotti (left_only: la cucitrice); un prodotto non ha alcun ordine (right_only: il pennarello). Sono le stesse informazioni del punto 5 del piano, prodotte direttamente dal merge stesso.
L'outer merge ha inoltre ordinato le righe in base alla chiave — bag, bottle, eraser, … — invece di mantenere l'ordine degli ordini. Gli inner e i left merge mantengono l'ordine della tabella di sinistra; l'outer non lo garantisce. Se l'ordine è importante, esegui l'ordinamento successivamente, come nel capitolo sette.
La colonna _merge contiene dati ordinari, quindi puoi filtrarla. Elencare solo i problemi richiede una sola riga:
problems = audit[audit["_merge"] != "both"]
print(problems[["order_id", "product", "_merge"]])order_id product _merge
6 NaN marker right_only
12 1011.0 stapler left_onlyEsegui questo tipo di verifica ogni volta che unisci dati che non hai creato tu. Quando l'elenco dei problemi è vuoto, hai la prova che i due file concordano.
Il pericolo silenzioso: le chiavi duplicate
Finora products aveva una riga per prodotto. Supponiamo che l'ufficio acquisti aggiunga un secondo fornitore di penne a un costo leggermente più alto e si limiti ad aggiungere una riga in fondo, così che pen compaia due volte:
products_dup = pd.concat(
[products, pd.DataFrame({"product": ["pen"], "cost": [10.0], "supplier": ["Gamma"]})],
ignore_index=True,
)
print(products_dup["product"].is_unique)
exploded = pd.merge(march, products_dup, on="product", how="left")
print(march.shape, "->", exploded.shape)
print(exploded[["order_id", "product", "quantity", "cost", "supplier"]])False
(8, 8) -> (10, 10)
order_id product quantity cost supplier
0 1001 pen 12 9.0 Alpha
1 1001 pen 12 10.0 Gamma
2 1002 notebook 5 40.0 Alpha
3 1003 bag 2 600.0 Bravo
4 1004 bottle 7 80.0 Bravo
5 1005 eraser 30 5.0 Alpha
6 1006 bag 1 600.0 Bravo
7 1007 pen 20 9.0 Alpha
8 1007 pen 20 10.0 Gamma
9 1008 bottle 4 80.0 BravoMarzo contava 8 ordini; il merge ha restituito 10 righe. Gli ordini 1001 e 1007 — i due ordini di penne — compaiono ciascuno due volte, una per ciascuna riga della penna. Nulla è fallito. Ma sommando ora la quantità, le penne vengono contate due volte: 32 penne in più che non sono mai state vendute.
print(march["quantity"].sum(), exploded["quantity"].sum())81 11381 contro 113. Questo fenomeno si chiama esplosione delle righe, ed è l'errore di merge più costoso perché il risultato sembra del tutto normale. La regola alla base è questa: il merge abbina ogni riga corrispondente di sinistra con ogni riga corrispondente di destra. Una relazione uno a uno (one-to-one) o molti a uno (many-to-one) mantiene invariato il numero di righe; una chiave ripetuta su entrambi i lati lo moltiplica.
Un merge che aggiunge righe non lo segnala mai. L'unico sintomo è un totale eccessivamente alto, e un totale eccessivamente alto raramente sembra sbagliato. Confronta len() prima e dopo ogni merge — è l'unico controllo capace di intercettare questo errore.validate= — lascia che sia pandas a controllare
Puoi specificare la relazione che ti aspetti, e pandas rifiuterà di eseguire il merge se i dati non la rispettano. Molti ordini condividono lo stesso prodotto, quindi questo merge dovrebbe essere molti a uno:
try:
pd.merge(march, products_dup, on="product", how="left", validate="many_to_one")
except Exception as error:
print(type(error).__name__ + ":", error)MergeError: Merge keys are not unique in right dataset; not a many-to-one merge
Duplicates in right:
product
pen ...Un MergeError, che elenca persino la chiave problematica. Gli altri valori possibili sono "one_to_one", "one_to_many" e "many_to_many". Scrivere validate= non costa nulla e trasforma una risposta silenziosamente errata in un errore palese: perciò usalo in ogni merge con una tabella di lookup.
Risolvere il problema: decidere quale duplicato sia corretto
L'errore ti dice che qualcosa non va; decidere cosa sia corretto spetta a te. Per prima cosa, osserva i duplicati:
print(products_dup[products_dup["product"].duplicated(keep=False)])product cost supplier
0 pen 9.0 Alpha
6 pen 10.0 Gammaduplicated(keep=False) contrassegna ogni copia, non solo la seconda. A questo punto c'è una decisione di business da prendere. Se la riga più recente sostituisce quella vecchia, mantieni l'ultima copia:
products_clean = products_dup.drop_duplicates(subset="product", keep="last")
fixed = pd.merge(march, products_clean, on="product", how="left", validate="many_to_one")
print(fixed.shape)
print(fixed.loc[fixed["product"] == "pen", ["order_id", "cost", "supplier"]])(8, 10)
order_id cost supplier
0 1001 10.0 Gamma
6 1007 10.0 GammaDi nuovo 8 righe, una per ordine. Se invece entrambi i fornitori vendono realmente penne e l'ordine specifica quale fornitore, allora supplier appartiene anche agli ordini, e la chiave di merge diventa composta da due colonne: on=["product", "supplier"]. Il codice non può fare questa scelta per te; la pianificazione sì.
Quando le colonne chiave hanno nomi diversi
branches.csv chiama la propria colonna branch_name, mentre gli ordini la chiamano branch. on= richiede lo stesso nome su entrambi i lati, quindi specifica ciascun lato separatamente con left_on= e right_on=:
branches = pd.read_csv("branches.csv")
with_manager = pd.merge(orders, branches, left_on="branch", right_on="branch_name", how="left")
print(list(with_manager.columns))['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price', 'month', 'branch_name', 'manager']Entrambe le colonne chiave vengono mantenute, perché pandas non può sapere che le consideri la stessa colonna. branch e branch_name contengono ora valori identici, quindi eliminane una:
with_manager = with_manager.drop(columns="branch_name")
print(with_manager[["order_id", "branch", "manager"]].head(4))order_id branch manager
0 1001 north Mira
1 1002 south Omar
2 1003 north Mira
3 1004 east LenaL'alternativa è rinominare prima del merge — branches.rename(columns={"branch_name": "branch"}) — e poi usare semplicemente on="branch". Entrambe le soluzioni sono valide; rinominare prima mantiene il risultato pulito senza bisogno di un passaggio successivo di pulizia.
Quando entrambe le tabelle hanno una colonna con lo stesso nome
Se una colonna diversa dalla chiave esiste in entrambe le tabelle, pandas non può inserire due colonne chiamate price nella stessa tabella, quindi le rinomina con i suffissi _x (sinistra) e _y (destra). Immagina una tabella con i prezzi di listino che chiama anch'essa la propria colonna price:
list_prices = pd.DataFrame({"product": ["pen", "bag"], "price": [14.0, 800.0]})
compared = pd.merge(march, list_prices, on="product")
print(compared[["order_id", "product", "price_x", "price_y"]])order_id product price_x price_y
0 1001 pen 15.0 14.0
1 1003 bag 850.0 800.0
2 1006 bag 850.0 800.0
3 1007 pen 15.0 14.0price_x e price_y funzionano, ma tra tre settimane nessuno ricorderà quale sia l'una e quale l'altra. Assegna loro un nome chiaro usando suffixes=:
compared = pd.merge(march, list_prices, on="product", suffixes=("_sold", "_list"))
print(compared[["order_id", "price_sold", "price_list"]])order_id price_sold price_list
0 1001 15.0 14.0
1 1003 850.0 800.0
2 1006 850.0 800.0
3 1007 15.0 14.0Ora le colonne dicono esattamente cosa rappresentano, e lo sconto applicato su ciascun ordine è semplicemente price_list - price_sold. (Il codice che in seguito richiedesse compared["price"] riceverebbe un KeyError, perché dopo il merge nessuna colonna ha esattamente quel nome — un altro motivo per scegliere i nomi deliberatamente.)
join — unire per indice, in breve
I DataFrame dispongono anche di un metodo .join(). Si tratta di un merge che usa l'indice della tabella di destra come chiave — comodo quando la tabella di lookup è già indicizzata per la chiave:
by_product = products.set_index("product")
joined = orders.join(by_product, on="product")
print(joined[["order_id", "product", "cost", "supplier"]].head(3))order_id product cost supplier
0 1001 pen 9.0 Alpha
1 1002 notebook 40.0 Alpha
2 1003 bag 600.0 BravoDi default join usa how="left". Svolge lo stesso compito di pd.merge(..., how="left"); merge è lo strumento più generale ed è quello utilizzato nel resto di questo corso.
Un esempio completo
Torniamo alla domanda della titolare del negozio: il profitto per fornitore tra marzo e aprile, tenendo conto di ogni singolo ordine.
supplier_profit.py:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
products = pd.read_csv("products.csv")
# 1. Stack the two months, remembering where each row came from.
march["month"] = "March"
april["month"] = "April"
orders = pd.concat([march, april], ignore_index=True)
print("orders:", orders.shape)
# 2. The lookup key must be unique, or the merge will duplicate orders.
assert products["product"].is_unique
# 3. Attach cost and supplier to every order. Keep every order.
sales = pd.merge(
orders, products, on="product", how="left",
validate="many_to_one", indicator=True,
)
print("after merge:", sales.shape)
assert len(sales) == len(orders)
# 4. Report the orders that found no product row, then set them aside.
unmatched = sales[sales["_merge"] == "left_only"]
print("no cost on file:", unmatched["order_id"].tolist(), unmatched["product"].tolist())
sales = sales[sales["_merge"] == "both"].drop(columns="_merge")
# 5. Profit per row, then per supplier and month.
sales["profit"] = sales["quantity"] * (sales["price"] - sales["cost"])
report = (
sales.groupby(["supplier", "month"], as_index=False)["profit"]
.sum()
.sort_values(["supplier", "profit"], ascending=[True, False])
.reset_index(drop=True)
)
print()
print(report)
print()
print(sales.groupby("supplier")["profit"].sum().sort_values(ascending=False))orders: (12, 8)
after merge: (12, 11)
no cost on file: [1011] ['stapler']
supplier month profit
0 Alpha March 382.0
1 Alpha April 150.0
2 Bravo March 1190.0
3 Bravo April 250.0
supplier
Bravo 1440.0
Alpha 532.0
Name: profit, dtype: float64Perché è scritto in questo modo:
- Ogni passaggio di combinazione è seguito dalla shape.
orders: (12, 8)conferma l'impilamento (8 + 4 righe, 7 colonne +month).after merge: (12, 11)conferma che nulla è stato scartato né duplicato — 3 nuove colonne:cost,supplier,_merge. Le shape sono la prova più economica che ciascun passaggio ha fatto quanto previsto dal piano. - I presupposti sono scritti sotto forma di codice.
assert products["product"].is_uniqueevalidate="many_to_one"proteggono entrambi dall'esplosione delle righe;assert len(sales) == len(orders)verifica la previsione del piano. Se qualcuno inviasse il file dei prodotti del mese prossimo contenente un duplicato, lo script si bloccherebbe invece di stampare un profitto gonfiato. how="left"mantiene l'ordine della cucitrice, eindicator=Truelo individua chiaramente. Lo script non lo elimina in silenzio; stampa[1011] ['stapler'], così la titolare sa che al totale di aprile manca una vendita finché l'ufficio acquisti non aggiunge un costo. Quella riga è parte integrante della risposta, non un output di debug.- La colonna
monthè stata aggiunta prima diconcat. Altrimenti la suddivisione per mese nel report avrebbe richiesto il parsing e la conversione delle date — operazione possibile, ma che richiede più lavoro per ottenere lo stesso risultato. - Il profitto viene calcolato dopo il merge, perché necessita di colonne provenienti da entrambi i file. Questo è l'ordine naturale: combinare, poi aggiungere colonne (capitolo nove), poi raggruppare (capitolo otto), infine ordinare (capitolo sette).
La risposta: Bravo ha generato 1.440 di profitto e Alpha 532, con una vendita di aprile ancora in attesa del rispettivo costo. Bravo vende pochi articoli, ma borse e bottiglie hanno un margine per articolo di gran lunga superiore a penne e gomme per cancellare.
Quando qualcosa va storto
ValueError: You are trying to merge on int64 and str columns for key 'order_id'. If you wish to proceed you should use pd.concat
La chiave ha un tipo diverso su ciascun lato. Ecco un file di consegne i cui ID ordine sono stati esportati con un # iniziale. Salvalo come deliveries.csv — viene usato anche nella sezione "Tocca a te". (Se hai ancora il registro dei fattorini con lo stesso nome del capitolo sui dati mancanti, sovrascrivilo; questo è un file diverso.)
order_id,status
#1001,delivered
#1002,delivered
#1003,returned
#1005,delivered
#1006,pendingimport pandas as pd
march = pd.read_csv("orders.csv")
deliveries = pd.read_csv("deliveries.csv")
print(march["order_id"].dtype, deliveries["order_id"].dtype)
try:
pd.merge(march, deliveries, on="order_id", how="left")
except ValueError as error:
print("ValueError:", error)int64 str
ValueError: You are trying to merge on int64 and str columns for key 'order_id'. If you wish to proceed you should use pd.concat1001 e "#1001" sono valori diversi e pandas rifiuta di tirare a indovinare. (Il suggerimento su pd.concat si riferisce a una situazione diversa; ignoralo.) La soluzione consiste nel rendere le chiavi dello stesso tipo e con lo stesso testo — rimuovi il #, poi converti:
deliveries["order_id"] = deliveries["order_id"].str.removeprefix("#").astype("int64")
status = pd.merge(march, deliveries, on="order_id", how="left")
print(status[["order_id", "product", "status"]])order_id product status
0 1001 pen delivered
1 1002 notebook delivered
2 1003 bag returned
3 1004 bottle NaN
4 1005 eraser delivered
5 1006 bag pending
6 1007 pen NaN
7 1008 bottle NaNGli ordini senza alcuna registrazione di consegna ricevono NaN per status — il left merge li mantiene, che è esattamente ciò che desideri quando la domanda è "quali ordini non sono stati consegnati?"
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge Hai usato validate="many_to_one" e la tabella di destra ripete una chiave. Il messaggio elenca i duplicati. Non rimuovere validate per far sparire l'errore — esamina le righe duplicate con duplicated(keep=False) e decidi quale sia corretta, come nella sezione sull'esplosione delle righe.
KeyError: 'branch' La colonna specificata in on= è assente da una delle tabelle:
branches = pd.read_csv("branches.csv")
try:
pd.merge(march, branches, on="branch")
except KeyError as error:
print("KeyError:", error)KeyError: 'branch'branches la chiama branch_name. Stampa list(df.columns) per entrambe le tabelle; usa left_on="branch", right_on="branch_name", oppure rinomina una delle due parti. Uno spazio finale nel nome di una colonna ("branch ") causa lo stesso errore ed è visibile solo stampando l'elenco.
Il merge è stato eseguito, ma le nuove colonne contengono tutte NaN Nessun errore, eppure nessuna riga trova corrispondenza. Le chiavi sembrano identiche ma non lo sono: "north " con uno spazio finale, "South" con la lettera maiuscola, oppure lo stesso ID rappresentato come testo in un file e con un prefisso nell'altro. Verifica con isin prima del merge:
branches_messy = pd.DataFrame({"branch_name": ["north ", "South", "east"], "manager": ["Mira", "Omar", "Lena"]})
print(march["branch"].isin(branches_messy["branch_name"]).sum(), "of", len(march), "orders match")
branches_messy["branch_name"] = branches_messy["branch_name"].str.strip().str.lower()
print(march["branch"].isin(branches_messy["branch_name"]).sum(), "of", len(march), "orders match")2 of 8 orders match
8 of 8 orders matchDue su otto prima della pulizia, otto su otto dopo. Pulisci la chiave allo stesso modo su entrambi i lati — str.strip() e un uso coerente di maiuscole/minuscole — prima di eseguire il merge.
Il numero di righe è aumentato dopo un merge La tabella di destra ha una chiave duplicata; vedi "Il pericolo silenzioso" sopra. Confronta len() prima e dopo ogni merge, e usa validate=.
Delle righe sono sparite dopo un merge Hai usato il valore predefinito how="inner", e alcune chiavi non avevano corrispondenza. Usa how="left" posizionando la tabella principale a sinistra, e usa indicator=True per vedere quali righe non hanno trovato corrispondenza.
TypeError: concat() takes 1 positional argument but 2 were given
try:
pd.concat(march, deliveries)
except TypeError as error:
print("TypeError:", error)TypeError: concat() takes 1 positional argument but 2 were givenconcat accetta un solo argomento, una lista di tabelle. Scrivi pd.concat([march, april]) — le parentesi quadre sono l'elemento fondamentale.
Un indice duplicato dopo concat, e loc restituisce due righe Ogni tabella in ingresso ha mantenuto i propri 0, 1, 2, …. Aggiungi ignore_index=True.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Marzo ha 8 ordini e aprile 4. Cosa viene stampato?
from io import StringIO
import pandas as pd
# Stand in for orders.csv, orders_april.csv and products.csv,
# so this snippet runs on its own.
MARCH = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
APRIL = """order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0
"""
PRODUCTS = """product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alpha
"""
march = pd.read_csv(StringIO(MARCH))
april = pd.read_csv(StringIO(APRIL))
products = pd.read_csv(StringIO(PRODUCTS))
both = pd.concat([march, april])
print(both.shape, both.index[-1])- A(12, 7) 11
- B(12, 14) 3
- C(12, 7) 3
- D(8, 7) 7
Qualcuno ha aggiunto una seconda riga per pen all'elenco dei prodotti. Cosa viene stampato?
from io import StringIO
import pandas as pd
# Stand in for orders.csv, orders_april.csv and products.csv,
# so this snippet runs on its own.
MARCH = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
APRIL = """order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0
"""
PRODUCTS = """product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alpha
"""
march = pd.read_csv(StringIO(MARCH))
april = pd.read_csv(StringIO(APRIL))
products = pd.read_csv(StringIO(PRODUCTS))
products = pd.concat([products, pd.DataFrame({"product": ["pen"], "cost": [10.0], "supplier": ["Charlie"]})])
report = pd.merge(march, products, on="product", how="left")
print(len(march), len(report))- A8 6
- B8 8
- CUn `MergeError` relativo a chiavi duplicate
- D8 10 — ogni ordine di penne ha trovato corrispondenza con entrambe le righe della penna ed è stato raddoppiato, senza alcun errore
Ogni ordine di marzo ha bisogno del costo del rispettivo prodotto e nessun ordine deve andare perduto, anche se il suo prodotto non è presente nell'elenco. Quale how= usi?
- A`how="outer"`
- B`how="right"`
- C`how="inner"`
- D`how="left"`, con gli ordini come tabella di sinistra
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Il team delle consegne ha inviato deliveries.csv (mostrato in "Quando qualcosa va storto") — una riga per ciascun ordine di cui possiede una registrazione, con gli ID ordine scritti come #1001. La titolare del negozio desidera un report sulle consegne che copra marzo e aprile insieme.
Scrivi delivery_report.py in modo che:
- Ogni ordine di entrambi i mesi venga mantenuto — esattamente una riga per ordine, verificata con un
assertsul conteggio delle righe, con il merge protetto davalidate=e sottoposto ad audit conindicator=True. - Le chiavi corrispondano realmente — la chiave delle consegne viene pulita per avere lo stesso tipo di
order_id, e lo script si interrompe se un qualsiasi ordine compare due volte nel file delle consegne. - Mancante significa "nessuna registrazione" — gli ordini privi di una riga di consegna ricevono lo stato
"no record", mai un silenziosoNaN. - Vengano stampati, in quest'ordine: la shape dopo l'impilamento, la shape dopo il merge, i conteggi di
_merge, il numero di ordini per ciascuno stato, gli ordini senza alcuna registrazione (order_id,branch,product, ordinati perorder_id) e il numero di ordini consegnati per ciascuna filiale.
Quando il programma è corretto, l'output è esattamente:
orders: (12, 7)
after merge: (12, 9)
_merge
left_only 7
both 5
right_only 0
Name: count, dtype: int64
status
no record 7
delivered 3
returned 1
pending 1
Name: count, dtype: int64
order_id branch product
3 1004 east bottle
6 1007 east pen
7 1008 north bottle
8 1009 north pen
9 1010 east notebook
10 1011 south stapler
11 1012 north bag
branch
north 2
south 1
dtype: int64Prima di scrivere qualsiasi riga di codice, spiega ciascun numero presente in quell'output basandoti esclusivamente sui file: perché 12 righe dopo l'impilamento, perché sempre 12 dopo il merge, e perché 7 ordini senza registrazione. Se non ci riesci, significa che il piano non è ancora completo.
Poi rompi il codice intenzionalmente, due volte:
- Rimuovi
.str.removeprefix("#"). Quale errore ottieni, e quale riga lo genera? - Aggiungi una seconda riga per
#1003adeliveries.csv(ad esempio#1003,delivered). Cosa favalidate=a questo punto? Cosa avrebbe mostrato il report senza di esso?
Soluzione
Il piano. La domanda: "per ogni ordine di marzo e aprile, qual è lo stato della consegna?" — quindi il risultato ha una riga per ordine, 12 righe. Marzo e aprile hanno colonne identiche: impilali con concat e ignore_index=True (8 + 4 = 12). Il file delle consegne è indicizzato per ID ordine, ma come testo con un #, quindi la chiave deve essere pulita prima di poter corrispondere all'intero order_id. Ciascun ordine dovrebbe comparirvi al massimo una volta, quindi la relazione è uno a uno (one-to-one: ciascun ordine ha al massimo una riga di consegna, ciascuna riga di consegna un solo ordine). Ogni ordine deve sopravvivere, quindi gli ordini vanno a sinistra e il merge è how="left". Esistono cinque righe di consegna, tutte relative a marzo, quindi 12 − 5 = 7 ordini dovrebbero risultare senza registrazione.
delivery_report.py:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
deliveries = pd.read_csv("deliveries.csv")
# 1. Stack the months.
orders = pd.concat([march, april], ignore_index=True)
print("orders:", orders.shape)
# 2. Clean the key, then check it.
deliveries["order_id"] = deliveries["order_id"].str.removeprefix("#").astype("int64")
assert deliveries["order_id"].is_unique, "an order appears twice in deliveries.csv"
# 3. Keep every order.
report = pd.merge(
orders, deliveries, on="order_id", how="left",
validate="one_to_one", indicator=True,
)
print("after merge:", report.shape)
assert len(report) == len(orders)
print(report["_merge"].value_counts())
# 4. Missing status means the delivery team has no record.
report["status"] = report["status"].fillna("no record")
# 5. Orders per status.
print()
print(report["status"].value_counts())
# 6. Orders with no record.
missing = report.loc[report["status"] == "no record", ["order_id", "branch", "product"]]
print()
print(missing.sort_values("order_id"))
# 7. Delivered orders per branch.
delivered = report[report["status"] == "delivered"]
print()
print(delivered.groupby("branch").size())orders: (12, 7)
after merge: (12, 9)
_merge
left_only 7
both 5
right_only 0
Name: count, dtype: int64
status
no record 7
delivered 3
returned 1
pending 1
Name: count, dtype: int64
order_id branch product
3 1004 east bottle
6 1007 east pen
7 1008 north bottle
8 1009 north pen
9 1010 east notebook
10 1011 south stapler
11 1012 north bag
branch
north 2
south 1
dtype: int64Tutte le previsioni sono confermate: 12 righe dopo l'impilamento, 12 dopo il merge, 7 senza alcuna registrazione. I conteggi di _merge lo confermano in modo indipendente — 5 both, 7 left_only, 0 right_only (un left merge non produce mai right_only).
Note sulle scelte effettuate:
validate="one_to_one", non"many_to_one". Anche la tabella degli ordini ha ID ordine univoci, quindi entrambi i lati sono univoci. Questo controllo più rigoroso intercetterebbe anche un ID ordine duplicato negli ordini — ad esempio se il file di aprile ripetesse accidentalmente un ordine di marzo.- L'
assertsuis_uniqueviene posto prima del merge, con un messaggio esplicativo, così che l'eventuale errore indichi chiaramente cosa c'è che non va in parole semplici anziché in termini tecnici di merge. fillna("no record")viene applicato dopo il merge, mai prima: gli stati mancanti esistono solo dopo che gli ordini senza una riga di consegna sono stati mantenuti dal left merge.- La filiale east non ha ordini consegnati, quindi non compare affatto nell'ultima tabella. Il raggruppamento produce solo i gruppi effettivamente presenti nei dati — se la titolare ha bisogno di uno 0 per east, ciò richiede un merge dell'elenco delle filiali con questo risultato, con
how="left", e unfillna(0).
I due errori provocati. Senza removeprefix, la riga astype("int64") fallisce per prima con ValueError: invalid literal for int() with base 10: '#1001', perché "#1001" non può essere convertito in un numero — l'errore punta alla riga di pulizia, che è il punto a cui appartiene la correzione. Se elimini anche astype, è il merge stesso a generare ValueError: You are trying to merge on int64 and str columns for key 'order_id'. Con una seconda riga #1003, l'assert interrompe lo script con AssertionError: an order appears twice in deliveries.csv; rimuovendo l'assert, validate="one_to_one" solleva MergeError: Merge keys are not unique in right dataset; not a one-to-one merge. Senza nessuna delle due protezioni, il report avrebbe mostrato 13 righe per 12 ordini e avrebbe contato l'ordine 1003 due volte — una come returned, una come delivered — e ogni totale sottostante sarebbe risultato silenziosamente errato.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz