Indice / Practicas / EJ-7.2
EJ-7.2 · Pedidos por cliente en JSON jerarquico
El camino inverso al de EJ-7.1: de una tabla plana de
202 líneas a un JSON de tres niveles. Y la primera práctica del curso cuya salida
no es un CSV.
Qué hay que hacer
A partir de pedidos_lineas.csv, producir
pedidos_por_cliente.json: una lista de 15 clientes, cada uno con sus
pedidos, y cada pedido con sus ítems.
ELI5Explicado facil
La tabla es el listado del depósito: un renglón por producto despachado, con el nombre del cliente repetido en cada uno.
El JSON es el legajo del cliente: una carpeta por persona, adentro un sobre por pedido, y adentro del sobre los productos. La misma información, guardada para responder otra pregunta.
Archivos
- Entrada ·
pedidos_lineas.csv - Objetivo ·
pedidos_por_cliente.json - Herramienta ·
comparar.py
Los tres niveles
| Nivel | Campos | Cuántos |
|---|---|---|
| 1 · cliente | cliente_id, cliente, pedidos, total, detalle | 15 |
| 2 · pedido | pedido, lineas, total, items | 85 |
| 3 · ítem | producto, cantidad, precio, importe | 202 |
Esos tres números son las invariantes del ejercicio: 15, 85 y 202. Si alguno no da, se perdió o se duplicó algo al anidar.
Cómo saber si te salió
python comparar.py mi_salida.json pedidos_por_cliente.json
comparar.py detecta que el objetivo termina en .json y
compara las estructuras, no las líneas: dos archivos con las claves en
distinto orden son el mismo dato. Cuando difieren, te dice la ruta exacta -
[3].detalle[1].items[0].precio - y los dos valores.
Antes de arrancar, mirá los datos
| cliente_id | cliente | pedido | producto | cantidad | precio |
|---|---|---|---|---|---|
| 7 | Horacio Duarte | 1001 | Auriculares | 5 | 56.5 |
| 7 | Horacio Duarte | 1001 | Ratón | 2 | 15.58 |
| 7 | Horacio Duarte | 1001 | Disco SSD | 5 | 86.64 |
| 10 | Ciro Ortiz | 1002 | Teclado | 4 | 25.41 |
| 14 | Agustín Bravo | 1003 | Docking | 3 | 138.04 |
El contrato de salida
pedidos_por_cliente.json · 15 filas
| Columna | Tipo | Que lleva |
|---|---|---|
cliente_id | entero | Nivel 1: la clave del cliente |
cliente | texto | Nivel 1: su nombre, completado desde sus otras lineas si falta |
pedidos | entero | Nivel 1: cuantos pedidos distintos hizo |
total | decimal | Nivel 1: suma de importes del cliente, 2 decimales |
detalle | texto | Nivel 2: una lista de pedidos, cada uno con pedido, lineas, total e items |
detalle[].items | texto | Nivel 3: una lista con producto, cantidad, precio e importe por linea |
Que hacer con cada defecto
| Defecto | Donde | Accion | Adonde va |
|---|---|---|---|
| el nombre del cliente falta en una linea | 1 linea del cliente 15 | completar completar desde las otras lineas del mismo cliente_id | pedidos_por_cliente.json |
| cantidad o precio ausentes | 3 lineas | se se escriben como null y su importe tambien es null. No suman al total del pedido ni al del cliente | pedidos_por_cliente.json |
| un pedido con dos lineas del mismo producto | varios pedidos | NO NO se agrupan: son dos lineas distintas. El orden dentro del pedido se completa con precio y cantidad para que no quede empate | pedidos_por_cliente.json |
| una linea de 99.999 unidades y una cantidad negativa | 2 lineas | NO NO se validan: este ejercicio cambia el formato, no los datos | pedidos_por_cliente.json |
Una fila puede tener mas de un problema. Se informa solo el primero de esta lista:
- completar el nombre faltante
- calcular el importe de cada linea y redondearlo
- agrupar por cliente, y dentro por pedido
- escribir con json.dump y ensure_ascii=False
El redondeo va por linea, antes de sumar, para que los totales de los tres niveles sean consistentes entre si. Y los ausentes se convierten a None recien al armar el diccionario: un NaN de numpy que llega a json.dump se escribe como NaN, que NO es JSON valido.
Conteos clave
| lineas de entrada | 202 |
|---|---|
| clientes en el JSON | 15 |
| pedidos en total | 85 |
| lineas sin importe | 3 |
| por motivo |
Invariantes
Comprobaciones que tu resultado tiene que cumplir. Corrilas vos mismo antes de comparar los archivos:
len(datos) == 15
sum(c['pedidos'] for c in datos) == 85
sum(len(p['items']) for c in datos for p in c['detalle']) == 202
all(p['lineas'] == len(p['items']) for c in datos for p in c['detalle'])
all(c['pedidos'] == len(c['detalle']) for c in datos)
[c['cliente_id'] for c in datos] == sorted(c['cliente_id'] for c in datos)
all(c['cliente'] for c in datos)
PISTASSi te trabas, abri esto
- comparar.py detecta que el objetivo termina en .json y compara las estructuras, no las lineas: dos archivos con las claves en distinto orden son el mismo dato.
- Un NaN de numpy que llega a json.dump se escribe como NaN, que no es JSON valido. Hay que convertirlo a None antes.
- int() y float() sobre los valores de pandas: un np.int64 tampoco es serializable por json.dump.
- Las tres invariantes de conteo (15 clientes, 85 pedidos, 202 items) son lo que confirma que no se perdio ni se duplico ninguna linea al anidar.
Así tiene que quedar
[
{
"cliente_id": 1,
"cliente": "Fabián Zapata",
"pedidos": 6,
"total": 3517.96,
"detalle": [
{
"pedido": 1008,
"lineas": 3,
"total": 659.59,
"items": [
{
"producto": "Docking",
"cantidad": 1,
"precio": 146.41,
"importe": 146.41
},
{
"producto": "Docking",
"cantidad": 3,
"precio": 150.1,
"importe": 450.3
},
{
"producto": "Ratón",
"cantidad": 4,
"precio": 15.72,
"importe": 62.88
}
]
},
{
"pedido": 1033,
"lineas": 2,
"total": 209.95,
"items": [
{
"producto": "Ratón",
"cantidad": 1,
"precio": 14.95,
"importe": 14.95
},
{
"producto": "Webcam",
"cantidad": 5,
"precio": 39.0,
"importe": 195.0
}
]
},
{
"pedido": 1035,
"lineas": 3,
"total": 942.39,
"items": [
{
"producto": "Auriculares",
"cantidad": 3,
"precio": 56.59,
"importe": 169.77
},
{
"producto": "Monitor",
"cantidad": 2,
"precio": 191.01,
"importe": 382.02
},
{
"producto": "Monitor",
"cantidad": 2,
"precio": 195.3,
"importe": 390.6
}
]
},
{
"pedido": 1041,
"lineas": 2,
"total": 414.0,
"items": [
{
"producto": "Docking",
"cantidad": 2,
"precio": 148.74,
"importe": 297.48
},
{
"producto": "Webcam",
"cantidad": 3,
"precio": 38.84,
"importe": 116.52
}
]
},
{
"pedido": 1044,
"lineas": 2,
"total": 787.51,
"items": [
{
"producto": "Monitor",
"cantidad": 4,
"precio": 190.39,
"importe": 761.56
},
{
"producto": "Teclado",
"cantidad": 1,
"precio": 25.95,
"importe": 25.95
}
]
},
{
"pedido": 1050,
"lineas": 2,
"total": 504.52,
"items": [
{
"producto": "Disco SSD",
"cantidad": 5,
"precio": 89.48,
"importe": 447.4
},
{
"producto": "Ratón",
"cantidad": 4,
"precio": 14.28,
"importe": 57.12
}
]
}
]
},
{
"cliente_id": 2,
"cliente": "Diego Acosta",
"pedidos": 5,
"total": 3009.01,
"detalle": [
{
"pedido": 1026,
"lineas": 1,
"total": 14.49,
"items": [
{
"producto": "Ratón",
"cantidad": 1,
"precio": 14.49,
"importe": 14.49
}
]
},
{
"pedido": 1058,
"lineas": 3,
"total": 1236.62,
"items": [
{
"producto": "Docking",
"cantidad": 4,
"precio": 148.24,
"importe": 592.96
},
{
"producto": "Docking",
"cantidad": 3,
"precio": 149.57,
"importe": 448.71
},
{
"producto": "Monitor",
"cantidad": 1,
"precio": 194.95,
"importe": 194.95
}
]
},
{
"pedido": 1062,
"lineas": 1,
"total": 119.4,
"items": [
{
"producto": "Teclado",
"cantidad": 5,
"precio": 23.88,
"importe": 119.4
}
]
},
{
"pedido": 1063,
"lineas": 2,
"total": 503.84,
"items": [
{
"producto": "Auriculares",
"cantidad": 4,
"precio": 53.49,
"importe": 213.96
},
{
"producto": "Docking",
"cantidad": 2,
"precio": 144.94,
"importe": 289.88
}
]
},
{
"pedido": 1081,
"lineas": 4,
"total": 1134.66,
"items": [
{
"producto": "Disco SSD",
"cantidad": 3,
"precio": 91.4,
"importe": 274.2
},
{
"producto": "Monitor",
"cantidad": 4,
"precio": 187.48,
"importe": 749.92
},
{
"producto": "Teclado",
"cantidad": 3,
"precio": 23.99,
"importe": 71.97
},
{
"producto": "Webcam",
"cantidad": 1,
"precio": 38.57,
"importe": 38.57
}
]
}
]
}
]NaN no es JSON válido
Y json.dump lo escribe igual, sin quejarse: produce un archivo con la
palabra NaN suelta que ningún parser puede leer, ni
siquiera el de Python al volver a abrirlo.
Hay que convertirlo a None antes de armar el diccionario, para que
salga como null. Lo mismo con los tipos de numpy: un
np.int64 tampoco es serializable, y hay que pasarlo por
int().
Una lista de JSON tiene orden, así que las mismas reglas de siempre valen acá:
clientes por cliente_id, pedidos por número, e ítems por producto. Y como
un pedido puede tener dos líneas del mismo producto, el orden se
completa con precio y cantidad.