Saltar al contenido

Indice / Practicas / EJ-7.2

EJ-7.2 · Pedidos por cliente en JSON jerarquico

Practica de: 7.2 JSON con pandas 6.4 groupbyDificultad: M

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

Los tres niveles

Qué lleva cada nivel
NivelCamposCuántos
1 · clientecliente_id, cliente, pedidos, total, detalle15
2 · pedidopedido, lineas, total, items85
3 · ítemproducto, cantidad, precio, importe202

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

pedidos_lineas.csv (entrada) · 202 filas, se muestran 5
cliente_idclientepedidoproductocantidadprecio
7Horacio Duarte1001Auriculares556.5
7Horacio Duarte1001Ratón215.58
7Horacio Duarte1001Disco SSD586.64
10Ciro Ortiz1002Teclado425.41
14Agustín Bravo1003Docking3138.04

El contrato de salida

pedidos_por_cliente.json · 15 filas

ColumnaTipoQue lleva
cliente_identeroNivel 1: la clave del cliente
clientetextoNivel 1: su nombre, completado desde sus otras lineas si falta
pedidosenteroNivel 1: cuantos pedidos distintos hizo
totaldecimalNivel 1: suma de importes del cliente, 2 decimales
detalletextoNivel 2: una lista de pedidos, cada uno con pedido, lineas, total e items
detalle[].itemstextoNivel 3: una lista con producto, cantidad, precio e importe por linea

Orden de las filas: cliente_id ascendente en el nivel 1; pedido ascendente en el nivel 2; producto, precio y cantidad en el nivel 3. Indice: no aplica: es un JSON. Se escribe con json.dump y no con to_csv(index=False)

Que hacer con cada defecto

DefectoDondeAccionAdonde va
el nombre del cliente falta en una linea1 linea del cliente 15completar completar desde las otras lineas del mismo cliente_idpedidos_por_cliente.json
cantidad o precio ausentes3 lineasse se escriben como null y su importe tambien es null. No suman al total del pedido ni al del clientepedidos_por_cliente.json
un pedido con dos lineas del mismo productovarios pedidosNO NO se agrupan: son dos lineas distintas. El orden dentro del pedido se completa con precio y cantidad para que no quede empatepedidos_por_cliente.json
una linea de 99.999 unidades y una cantidad negativa2 lineasNO NO se validan: este ejercicio cambia el formato, no los datospedidos_por_cliente.json
Orden de comprobacion

Una fila puede tener mas de un problema. Se informa solo el primero de esta lista:

  1. completar el nombre faltante
  2. calcular el importe de cada linea y redondearlo
  3. agrupar por cliente, y dentro por pedido
  4. 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

Si estos numeros no te dan, algo se desvio antes de que llegues a comparar los archivos.

lineas de entrada202
clientes en el JSON15
pedidos en total85
lineas sin importe3
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

pedidos_por_cliente.json (objetivo) · 2 de 15 registros
[
  {
    "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().

El orden también hay que declararlo en un JSON

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.