Les tres lliçons anteriors han consolidat, exercici a exercici, els algorismes (09-01), l'aprenentatge automàtic (09-02) i les xarxes neuronals (09-03). Falta el que 08-01 va anomenar "el mètode": portar un cas complet des de la pregunta de negoci fins a un prototip documentat i presentable, prenent totes les decisions intermèdies sense que la lliçó les prengui per tu. Aquesta lliçó és aquesta pràctica. Resolem de principi a fi el cas 9 de NovaMarket, el diagnòstic de la causa de les incidències, que fins ara només tenia la xarxa bayesiana de 06-03 i les regles de 06-04: generarem un incidencies.csv sintètic coherent amb aquelles taules de probabilitat, mesurarem la xarxa com a línia base, entrenarem un classificador amb columnes que la xarxa no veu, els combinarem amb regles de negoci en cues de treball, escriurem la model card i una comprovació de deriva, i prepararem la presentació al Diego. Al final trobaràs el guió equivalent per al cas 4 (classificar ressenyes i prioritzar la resposta d'atenció al client), per si prefereixes fer el projecte amb text.

Com treballar la lliçó: és un projecte de 8-12 hores repartides en diverses sessions. Llegeix les regles del joc, fes tu cada pas (document, codi, taules) i només després compara amb la nostra versió. En acabar, autoavalua't amb la rúbrica. Necessites l'entorn del mòdul 7 (pandas, scikit-learn, joblib) i, si fas l'alternativa, ressenyes_nm.py.

Contingut

  1. Regles del joc: temps, lliurables i rúbrica d'autoavaluació
  2. Definició del problema: el document d'una pàgina
  3. Dades: incidencies.csv sintètic coherent amb la xarxa de 06-03
  4. Línia base: la xarxa bayesiana per enumeració
  5. Classificador supervisat amb les columnes extra i comparació honesta
  6. Combinació neurosimbòlica: regles de negoci i cues de treball
  7. Model card i comprovació de deriva
  8. Presentació de resultats al Diego
  9. Rúbrica final i guió per al cas 4
  10. Errors Comuns i Consells
  11. Conclusió

  1. Regles del joc: temps, lliurables i rúbrica d'autoavaluació

Temps suggerit: 8-12 hores en 4-6 sessions (definició i dades, línia base i classificador, regles i cues, documentació i presentació).

Lliurables (una carpeta novamarket_ia/incidencies/ amb l'estructura de 07-04):

  1. DEFINICIO.md: el document de definició d'una pàgina (secció 1).
  2. dades.py amb el generador reproduïble i data/raw/incidencies.csv (fora de Git si fos real).
  3. linia_base.py, entrenar.py, decidir.py, deriva.py: codi executable amb sortides.
  4. models/model_card.json i models/referencia_deriva.json.
  5. PRESENTACIO.md: el guió de 5 diapositives.
  6. Aquesta rúbrica emplenada per tu.
Criteri 1 (insuficient) 2 (acceptable) 3 (bé) 4 (excel·lent)
Definició Només "predir la causa" Pregunta i decisió escrites + mètrica de negoci i tècnica, línia base + restriccions legals/ètiques i criteri d'èxit verificable
Dades Generador sense llavor ni coherència Reproduïble + coherent amb les CPT, amb etiquetes parcials + comprovació numèrica de la coherència i fuites identificades
Línia base No n'hi ha Majoritària Xarxa bayesiana avaluada + anàlisi per classe i per confiança
Model Un model sense validar Validació creuada + comparació amb la línia base a les mateixes files + corba d'aprenentatge o anàlisi de quina columna aporta
Combinació Només el model Llindar de revisió + regles de negoci amb traça + cues mesurades (mida i encert) i efecte del llindar
Documentació Res Model card mínima + referència i comprovació de deriva + calendari de monitoratge i limitacions honestes
Comunicació Xifres soltes Taula de resultats Guió de 5 diapositives + una decisió demanada al Diego i riscos

  1. Definició del problema: el document d'una pàgina

Recordatori (08-01, seccions 2 i 11.1). Abans de tocar una dada: quina pregunta respon el sistema, quina decisió canvia, mètrica de negoci i tècnica, línia base, restriccions, criteri d'èxit. Una taula d'una pàgina signada per negoci (el Diego) i dades (la Marta).

Camp Contingut
Pregunta Donada una incidència acabada d'obrir (magatzem, símptomes declarats, dies des de l'enviament, transportista, valor), quina és la seva causa més probable i amb quina confiança?
Decisió que canvia Avui l'agent llegeix el tiquet i decideix a mà. Amb el sistema, cada incidència entra en una de dues cues: automàtica (causa acceptada, accions llançades: reenviament, traça amb el transportista, avís a compres) o revisió humana (l'agent decideix amb la proposta i la traça a la vista). Les accions automàtiques són totes reversibles.
Mètrica de negoci Minuts d'agent per incidència (una incidència automàtica costa ~2 min; una de revisada, ~8) i encert a la cua automàtica ≥ 85 % (un diagnòstic erroni automàtic genera una segona incidència).
Mètrica tècnica Exactitud i F1 macro sobre les cinc causes; log-loss (calibratge) perquè les regles fan servir la probabilitat.
Línia base Majoritària ("sempre error de picking", 31 %) i la xarxa bayesiana de 06-03 amb les CPT del Diego (sense dades etiquetades).
Criteri d'èxit Automatitzar ≥ 60 % de les incidències amb ≥ 85 % d'encert en aquesta cua; guanyar la xarxa bayesiana en exactitud; presentar traça llegible per incidència.
Restriccions La causa no es fa servir per sancionar ningú (empleats o transportistes) sense revisió humana; RGPD: sense dades personals del client al model; Reglament d'IA: risc mínim (suport intern). Només el 50 % de les incidències té la causa registrada.
Dades incidencies.csv (2.000 files, 3 mesos), símptomes, magatzem, dies, transportista, valor; etiqueta = causa registrada per l'agent en tancar.
Desplegament Lot cada hora sobre les incidències noves; sortida = cua + causa + accions + traça; ombra 2 setmanes a Zaragoza.
Propietari / responsable El Diego (operacions), la Marta (model), atenció al client (usuaris), compres i logística (accions).
Pressupost Prototip: aquesta pràctica; pilot: 4 setmanes.

  1. Dades: incidencies.csv sintètic coherent amb la xarxa de 06-03

Recordatori (06-03, seccions 6-7; 02-03). La xarxa té magatzem (zaragoza 0,60 / getafe 0,40) → causa (5 valors, CPT per magatzem) → quatre símptomes binaris amb P(símptoma | causa). Les dades reals tenen la causa només en part dels casos i columnes que la xarxa no modela; un generador reproduïble permet comprovar que el sistema recupera el que sabem que hi ha.

La teva tasca. Escriu generar_incidencies(n=2000, llavor=42, frac_etiquetada=0.5) que mostregi magatzem, causa i símptomes exactament de les CPT de 06-03 i afegeixi tres columnes fora de la xarxa: dies_des_enviament (Poisson amb mitjana segons la causa: picking 3, transport 4, adreça 8, stock 9, proveïdor 7, més 1), transportista (segons el magatzem: Zaragoza fa servir sobretot TransNova; Getafe, RapidEnvío) i valor_comanda (gamma, independent). Desa la causa real a part i deixa causa_registrada només a la meitat de les files. Comprova numèricament que les taxes coincideixen amb les CPT.

import numpy as np, pandas as pd, json
from itertools import product
pd.set_option("display.width", 200)

CAUSES = ["error_picking", "dany_transport", "adreca_incorrecta", "stock_desactualitzat", "fallada_proveidor"]
SIMPTOMES = ["paquet_danyat", "producte_equivocat", "no_arriba", "retard"]
P_MAGATZEM = {"zaragoza": 0.60, "getafe": 0.40}
P_CAUSA = {"zaragoza": [0.35, 0.30, 0.15, 0.08, 0.12], "getafe": [0.25, 0.20, 0.12, 0.33, 0.10]}     # CPT de 06-03
P_SIMPTOMA = {"paquet_danyat":      [0.10, 0.80, 0.02, 0.02, 0.15],                                  # P(si | causa)
              "producte_equivocat": [0.70, 0.02, 0.01, 0.10, 0.30],
              "no_arriba":          [0.05, 0.10, 0.85, 0.40, 0.30],
              "retard":             [0.10, 0.30, 0.60, 0.90, 0.70]}
DIES_MITJANA = {"error_picking": 3, "dany_transport": 4, "adreca_incorrecta": 8, "stock_desactualitzat": 9, "fallada_proveidor": 7}
P_TRANSPORTISTA = {"zaragoza": {"TransNova": 0.5, "RapidEnvio": 0.3, "LogiSur": 0.2},
                   "getafe":   {"RapidEnvio": 0.5, "LogiSur": 0.3, "TransNova": 0.2}}

def generar_incidencies(n=2000, llavor=42, frac_etiquetada=0.5, p_causa=None):
    """incidencies.csv sintetic coherent amb la xarxa de 06-03. 'causa' es la veritat; 'causa_registrada' nomes a frac_etiquetada."""
    rng = np.random.default_rng(llavor); p_causa = p_causa or P_CAUSA         # p_causa: per simular un mes diferent (deriva)
    magatzem = rng.choice(list(P_MAGATZEM), size=n, p=list(P_MAGATZEM.values()))
    causa = np.array([rng.choice(CAUSES, p=p_causa[a]) for a in magatzem])
    idx = np.array([CAUSES.index(c) for c in causa])
    df = pd.DataFrame({"id_incidencia": [f"I{7000 + i}" for i in range(n)], "magatzem": magatzem})
    for s in SIMPTOMES:
        df[s] = (rng.random(n) < np.array(P_SIMPTOMA[s])[idx]).astype(int)
    df["dies_des_enviament"] = np.clip(rng.poisson([DIES_MITJANA[c] for c in causa]) + 1, 1, 30)
    df["transportista"] = [rng.choice(list(P_TRANSPORTISTA[a]), p=list(P_TRANSPORTISTA[a].values())) for a in magatzem]
    df["valor_comanda"] = np.round(rng.gamma(2.0, 60.0, n) + 5, 2)
    df["causa"] = causa
    df["causa_registrada"] = np.where(rng.random(n) < frac_etiquetada, causa, None)
    return df

inc = generar_incidencies(); print(inc.shape, "| etiquetades:", inc["causa_registrada"].notna().sum())
print(inc.drop(columns=["causa"]).head(3).to_string())
print(inc.groupby("magatzem")["causa"].value_counts(normalize=True).unstack()[CAUSES].round(2).to_string())
print(inc.groupby("causa")[SIMPTOMES].mean().loc[CAUSES].round(2))
print("dies mitjans per causa:", inc.groupby("causa")["dies_des_enviament"].mean().round(1).to_dict())
etiq = inc[inc["causa_registrada"].notna()].copy(); y = etiq["causa_registrada"]
(2000, 11) | etiquetades: 1003
  id_incidencia  magatzem  paquet_danyat  producte_equivocat  no_arriba  retard  dies_des_enviament transportista  valor_comanda      causa_registrada
0         I7000    getafe              0                   0          0       1                   9    RapidEnvio          23.26  stock_desactualitzat
1         I7001  zaragoza              1                   0          0       0                   5     TransNova         100.02        dany_transport
2         I7002    getafe              0                   0          1       1                   7    RapidEnvio         119.75                   NaN
causa     error_picking  dany_transport  adreca_incorrecta  stock_desactualitzat  fallada_proveidor
magatzem
getafe             0.26            0.19               0.12                  0.32               0.11
zaragoza           0.37            0.30               0.14                  0.08               0.11
                      paquet_danyat  producte_equivocat  no_arriba  retard
causa
error_picking                  0.08                0.70       0.06    0.10
dany_transport                 0.83                0.01       0.11    0.32
adreca_incorrecta              0.02                0.02       0.83    0.58
stock_desactualitzat           0.03                0.11       0.40    0.90
fallada_proveidor              0.13                0.25       0.30    0.68
dies mitjans per causa: {'adreca_incorrecta': 9.0, 'dany_transport': 5.1, 'error_picking': 4.0, 'fallada_proveidor': 8.0, 'stock_desactualitzat': 10.2}

Les freqüències mostrals reprodueixen les CPT amb ±0,03 (2.000 files): el generador és coherent. Decisions de dades que has de deixar escrites: causa (la veritat) no existeix al fitxer real i només es fa servir aquí per avaluar la cua sense etiqueta al final; el model s'entrena únicament amb les 1.003 etiquetades; id_incidencia no és característica; no hi ha dades personals.

  1. Línia base: la xarxa bayesiana per enumeració

Recordatori (06-03, secció 7). inferir(consulta, evidencia) suma la conjunta sobre les variables ocultes i normalitza. Amb l'evidència completa (magatzem i els quatre símptomes) no hi ha ocultes: és Bayes directe. La xarxa no necessita etiquetes: les seves 29 xifres les va posar el Diego.

La teva tasca. Reutilitza XARXA i inferir de 06-03 amb les taules de la secció 2, escriu diagnostic_bayes(df) que retorni P(causa | magatzem, símptomes) per fila (només hi ha 2 × 2⁴ = 32 combinacions d'evidència: fes servir una memòria cau), avalua exactitud, F1 macro i log-loss sobre les 1.003 etiquetades davant de la majoritària, i analitza l'encert segons la confiança màxima.

from sklearn.metrics import accuracy_score, f1_score, log_loss, classification_report

def cpt_binaria(llista): return {(c,): {1: p, 0: round(1 - p, 4)} for c, p in zip(CAUSES, llista)}
XARXA = {"magatzem": ([], list(P_MAGATZEM), {(): P_MAGATZEM}),
         "causa": (["magatzem"], CAUSES, {(a,): dict(zip(CAUSES, P_CAUSA[a])) for a in P_MAGATZEM})}
for s in SIMPTOMES: XARXA[s] = (["causa"], [1, 0], cpt_binaria(P_SIMPTOMA[s]))
ORDRE = ["magatzem", "causa"] + SIMPTOMES

def prob_conjunta(assig):
    p = 1.0
    for node in ORDRE:
        pares, _, cpt = XARXA[node]; p *= cpt[tuple(assig[q] for q in pares)][assig[node]]
    return p

def inferir(consulta, evidencia):
    ocultes = [n for n in ORDRE if n != consulta and n not in evidencia]; res = {}
    for v in XARXA[consulta][1]:
        res[v] = sum(prob_conjunta(dict(evidencia, **{consulta: v}, **dict(zip(ocultes, comb))))
                     for comb in product(*[XARXA[n][1] for n in ocultes]))
    z = sum(res.values()); return {v: p / z for v, p in res.items()}

def diagnostic_bayes(df):
    cache, files = {}, []
    for _, r in df.iterrows():
        clau = (r["magatzem"],) + tuple(int(r[s]) for s in SIMPTOMES)
        if clau not in cache:
            cache[clau] = inferir("causa", dict({"magatzem": r["magatzem"]}, **{s: int(r[s]) for s in SIMPTOMES}))
        files.append(cache[clau])
    return pd.DataFrame(files, index=df.index)[CAUSES]

CLASSES = sorted(CAUSES)                                   # l'ordre alfabetic que fa servir scikit-learn
P_bayes = diagnostic_bayes(etiq); pred_bayes = P_bayes.idxmax(axis=1)
print("Majoritaria (error_picking):", round((y == "error_picking").mean(), 3))
print("Xarxa bayesiana: exactitud", round(accuracy_score(y, pred_bayes), 3), "| F1 macro", round(f1_score(y, pred_bayes, average="macro"), 3),
      "| log-loss", round(log_loss(y, P_bayes[CLASSES].values, labels=CLASSES), 3))
print(classification_report(y, pred_bayes, digits=2))
maxp = P_bayes.max(axis=1)
print(f"P maxima mitjana {maxp.mean():.3f} | files amb P ≥ 0,6: {(maxp >= 0.6).mean():.1%}, encert {accuracy_score(y[maxp >= 0.6], pred_bayes[maxp >= 0.6]):.3f} | P < 0,6: encert {accuracy_score(y[maxp < 0.6], pred_bayes[maxp < 0.6]):.3f}")
Majoritaria (error_picking): 0.314
Xarxa bayesiana: exactitud 0.719 | F1 macro 0.606 | log-loss 0.789
                      precision    recall  f1-score   support
   adreca_incorrecta       0.62      0.62      0.62       134
      dany_transport       0.88      0.82      0.85       257
       error_picking       0.78      0.90      0.83       315
   fallada_proveidor       0.50      0.05      0.09       118
stock_desactualitzat       0.54      0.77      0.64       179
            accuracy                           0.72      1003
           macro avg       0.66      0.63      0.61      1003
P maxima mitjana 0.723 | files amb P ≥ 0,6: 67.3%, encert 0.822 | P < 0,6: encert 0.506

La xarxa encerta el 71,9 % (davant del 31,4 % de la majoritària) sense haver vist ni una sola etiqueta: és el valor del coneixement expert. Els seus punts febles són clars: fallada_proveidor gairebé mai no es diagnostica (recall 0,05: els seus símptomes s'assemblen als de stock i de picking i el seu prior és el més baix), i stock/adreça es confonen entre si. I la confiança és informativa: quan P ≥ 0,6 (dos terços dels casos) encerta el 82 %; per sota, la meitat. Aquesta és la llavor de la banda de revisió de la secció 5. Com que les dades es van generar amb aquestes mateixes taules, la xarxa és el millor classificador possible amb aquestes cinc variables: qualsevol millora haurà de venir de les columnes que la xarxa no veu.

  1. Classificador supervisat amb les columnes extra i comparació honesta

Recordatori (04-04, 04-05, 09-02). Regressió logística multiclasse, arbre, bosc i gradient boosting sobre un ColumnTransformer; validació creuada estratificada; comparar a les mateixes files i per plec; mirar la matriu de confusió i quina característica aporta.

La teva tasca. Entrena i compara amb StratifiedKFold(5) quatre models sobre magatzem, transportista, símptomes, dies i valor; mostra que la millora ve de dies_des_enviament (treu columnes); compara amb la xarxa per plec; dibuixa una corba d'aprenentatge amb 100, 250, 500 i 1.003 etiquetes.

from sklearn.model_selection import StratifiedKFold, cross_val_predict
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, HistGradientBoostingClassifier

CATS, NUMS, BINS = ["magatzem", "transportista"], ["dies_des_enviament", "valor_comanda"], SIMPTOMES
COLS = CATS + NUMS + BINS
def crear_model(classificador=None, cats=CATS, nums=NUMS, bins=BINS):
    parts = ([("cat", OneHotEncoder(handle_unknown="ignore"), cats)] if cats else []) + \
            ([("num", StandardScaler(), nums)] if nums else []) + ([("bin", "passthrough", bins)] if bins else [])
    return Pipeline([("prep", ColumnTransformer(parts)), ("model", classificador if classificador is not None else LogisticRegression(max_iter=2000))])

cv = StratifiedKFold(5, shuffle=True, random_state=42)
def avaluar_cv(pipe, Xd, nom):
    P = cross_val_predict(pipe, Xd, y, cv=cv, method="predict_proba"); pred = np.array(CLASSES)[P.argmax(1)]
    print(f"{nom:36s} exactitud {accuracy_score(y, pred):.3f}  F1 macro {f1_score(y, pred, average='macro'):.3f}  log-loss {log_loss(y, P, labels=CLASSES):.3f}")
    return P, pred
models = {"Logística multiclasse": LogisticRegression(max_iter=2000), "Arbre (prof. 5)": DecisionTreeClassifier(max_depth=5, random_state=42),
          "Bosc (300, fulla 5)": RandomForestClassifier(n_estimators=300, min_samples_leaf=5, random_state=42),
          "HGB (prof. 3)": HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05, max_iter=200, random_state=42)}
resultats = {n: avaluar_cv(crear_model(m), etiq[COLS], n) for n, m in models.items()}
avaluar_cv(crear_model(cats=["magatzem"], nums=[], bins=BINS), etiq[["magatzem"] + BINS], "Logística NOMÉS magatzem + símptomes")
avaluar_cv(crear_model(cats=[], nums=["dies_des_enviament"], bins=[]), etiq[["dies_des_enviament"]], "Logística NOMÉS dies")
P_log, pred_log = resultats["Logística multiclasse"]
print(pd.crosstab(y, pred_log, rownames=["real"], colnames=["logística"]).to_string())
accs = [(accuracy_score(y.iloc[te], pred_bayes.iloc[te]), accuracy_score(y.iloc[te], pred_log[te])) for _, te in cv.split(etiq[COLS], y)]
print("Per plec (bayes, logística):", [(round(a, 3), round(b, 3)) for a, b in accs], "| avantatge mitjana", round(np.mean([b - a for a, b in accs]), 3))
for n_lab in (100, 250, 500, 1003):
    sub = etiq.sample(n_lab, random_state=1) if n_lab < len(etiq) else etiq
    P = cross_val_predict(crear_model(), sub[COLS], sub["causa_registrada"], cv=cv, method="predict_proba")
    print(f"n etiquetades={n_lab:4d}: logística {accuracy_score(sub['causa_registrada'], np.array(CLASSES)[P.argmax(1)]):.3f}  xarxa bayesiana {accuracy_score(sub['causa_registrada'], diagnostic_bayes(sub).idxmax(axis=1)):.3f}")
Logística multiclasse                exactitud 0.738  F1 macro 0.658  log-loss 0.690
Arbre (prof. 5)                      exactitud 0.690  F1 macro 0.598  log-loss 1.796
Bosc (300, fulla 5)                  exactitud 0.735  F1 macro 0.646  log-loss 0.759
HGB (prof. 3)                        exactitud 0.724  F1 macro 0.655  log-loss 0.802
Logística NOMÉS magatzem + símptomes exactitud 0.709  F1 macro 0.612  log-loss 0.804
Logística NOMÉS dies                 exactitud 0.454  F1 macro 0.292  log-loss 1.206
logística             adreca_incorrecta  dany_transport  error_picking  fallada_proveidor  stock_desactualitzat
real
adreca_incorrecta                    85               2              4                 15                    28
dany_transport                        3             211             26                 12                     5
error_picking                         2              10            291                  9                     3
fallada_proveidor                    24              15             20                 26                    33
stock_desactualitzat                 24               3              5                 20                   127
Per plec (bayes, logística): [(0.677, 0.716), (0.731, 0.721), (0.751, 0.761), (0.73, 0.77), (0.705, 0.72)] | avantatge mitjana 0.019
n etiquetades= 100: logística 0.780  xarxa bayesiana 0.790
n etiquetades= 250: logística 0.752  xarxa bayesiana 0.744
n etiquetades= 500: logística 0.764  xarxa bayesiana 0.746
n etiquetades=1003: logística 0.738  xarxa bayesiana 0.719

Lectura honesta:

  • La logística multiclasse és la millor (0,738; F1 macro 0,658; log-loss 0,690, la més ben calibrada), seguida del bosc; l'arbre sol està mal calibrat (log-loss 1,8) i HGB no aporta res amb 1.000 files. L'avantatge sobre la xarxa bayesiana és de +1,9 punts, a favor en 4 de 5 plecs: petit però real.
  • D'on ve: la logística només amb magatzem i símptomes dona 0,709, una mica per sota de la xarxa (0,719), com ha de ser (la xarxa és el model generador exacte d'aquestes variables); dies_des_enviament sola classifica el 45 %, i és la que suma en combinar-se: fallada_proveidor passa de recall 0,05 a 0,22 i stock/adreça se separen una mica millor. transportista i valor_comanda no aporten res (per construcció no depenen de la causa: comprovar-ho és part de la feina).
  • La corba d'aprenentatge explica la història de 06-03: amb 100 etiquetes la xarxa de l'expert guanya (0,79 davant de 0,78); a partir d'unes 250 el model après la iguala i amb 1.000 la supera. Amb poques etiquetes, coneixement; amb moltes, dades; i l'ideal (variant) és el neurosimbòlic també aquí: estimar les CPT de la xarxa des de les etiquetes amb suavitzat de Laplace i afegir-hi un node dies discretitzat.
  • Decisió: la logística multiclasse, per mètrica, calibratge i explicabilitat (els seus coeficients són llegibles i les seves probabilitats alimenten les regles de la secció 5); la xarxa es queda com a línia base documentada i com a suport si l'etiquetatge es degrada.

  1. Combinació neurosimbòlica: regles de negoci i cues de treball

Recordatori (06-04, seccions 9 i 11; 08-01, 11.4). El model és un sensor que retorna probabilitats; les regles converteixen aquestes probabilitats en fets amb una banda de revisió humana, apliquen la política i deixen traça llegible. La sortida operativa és una taula de cues.

La teva tasca. Escriu decidir(fila, probs) amb aquestes regles i avalua les cues amb les probabilitats de validació creuada: R0 comandes de més de 300 € sempre a revisió humana; R1 confiança < 0,6 → revisió humana; R2 confiança ≥ 0,6 → cua automàtica amb la causa acceptada; R3 dany en transport → obrir traça amb el transportista; R4 error de picking → reenviament des del magatzem; R5 "no arriba" i més de 10 dies → reclamació al transportista sigui quina sigui la causa; R6 stock desactualitzat → avís a compres. Mesura mida i encert de cada cua, l'efecte del llindar, i aplica el model final a les 997 incidències sense etiqueta.

LLINDAR_AUTO, VALOR_ALT, DIES_MAX = 0.60, 300.0, 10
def decidir(fila, probs):
    causa, p = CLASSES[int(np.argmax(probs))], float(np.max(probs)); accions, traca = [], []
    if fila["valor_comanda"] > VALOR_ALT:
        cua = "revisio_humana"; traca.append(f"R0: valor {fila['valor_comanda']:.0f} € > {VALOR_ALT:.0f} € → sempre ho revisa una persona")
    elif p < LLINDAR_AUTO:
        cua = "revisio_humana"; traca.append(f"R1: P({causa})={p:.2f} < {LLINDAR_AUTO} → zona grisa")
    else:
        cua = "automatica"; traca.append(f"R2: P({causa})={p:.2f} ≥ {LLINDAR_AUTO} → causa acceptada com a fet")
    if causa == "dany_transport" and p >= LLINDAR_AUTO:
        accions.append(f"obrir_traca:{fila['transportista']}"); traca.append("R3: dany en transport → traca amb el transportista")
    if causa == "error_picking" and p >= LLINDAR_AUTO:
        accions.append(f"reenviament_des_de:{fila['magatzem']}"); traca.append("R4: error de picking → reenviament immediat i avis al magatzem")
    if fila["no_arriba"] == 1 and fila["dies_des_enviament"] > DIES_MAX:
        accions.append("reclamacio_transportista"); traca.append(f"R5: no arriba i {fila['dies_des_enviament']} dies > {DIES_MAX} → reclamacio, sigui quina sigui la causa")
    if causa == "stock_desactualitzat" and p >= LLINDAR_AUTO:
        accions.append("avis_compres"); traca.append("R6: stock desactualitzat → avis a compres i alternativa al client")
    return {"cua": cua, "causa_proposta": causa, "confianca": round(p, 3), "accions": accions, "traca": traca}

decisions = pd.DataFrame([decidir(f, P_log[i]) for i, (_, f) in enumerate(etiq.iterrows())], index=etiq.index)
etiq2 = pd.concat([etiq, decisions], axis=1)
for cua, g in etiq2.groupby("cua"):
    print(f"{cua:16s} n={len(g):4d} ({len(g) / len(etiq2):.1%})  encert de la causa proposta {accuracy_score(g['causa_registrada'], g['causa_proposta']):.3f}")
print("accions llancades:", pd.Series([a.split(':')[0] for l in etiq2["accions"] for a in l]).value_counts().to_dict())
r = etiq2.iloc[6]; print(r["id_incidencia"], "| real:", r["causa_registrada"], "→", r["cua"], r["causa_proposta"], r["confianca"], r["accions"]); [print("   ", t) for t in r["traca"]]
for u in (0.5, 0.6, 0.7, 0.8):
    auto = etiq2[(P_log.max(1) >= u) & (etiq2["valor_comanda"] <= VALOR_ALT)]
    print(f"llindar {u}: automatica {len(auto) / len(etiq2):.1%}, encert {accuracy_score(auto['causa_registrada'], auto['causa_proposta']):.3f}")
model = crear_model().fit(etiq[COLS], y)                                        # model final amb les 1.003 etiquetades
sense = inc[inc["causa_registrada"].isna()].copy(); P_sense = model.predict_proba(sense[COLS])
dec_sense = pd.DataFrame([decidir(f, P_sense[i]) for i, (_, f) in enumerate(sense.iterrows())], index=sense.index)
auto = dec_sense["cua"] == "automatica"
print("Sense etiqueta:", dec_sense["cua"].value_counts().to_dict(), "| encert real (ocult) automatica:", round(accuracy_score(sense.loc[auto, "causa"], dec_sense.loc[auto, "causa_proposta"]), 3),
      "| humana:", round(accuracy_score(sense.loc[~auto, "causa"], dec_sense.loc[~auto, "causa_proposta"]), 3))
automatica       n= 649 (64.7%)  encert de la causa proposta 0.858
revisio_humana   n= 354 (35.3%)  encert de la causa proposta 0.517
accions llancades: {'reenviament_des_de': 290, 'obrir_traca': 223, 'avis_compres': 104, 'reclamacio_transportista': 67}
I7010 | real: error_picking → revisio_humana error_picking 0.808 ['reenviament_des_de:zaragoza']
    R0: valor 349 € > 300 € → sempre ho revisa una persona
    R4: error de picking → reenviament immediat i avis al magatzem
llindar 0.5: automatica 76.5%, encert 0.821
llindar 0.6: automatica 64.7%, encert 0.858
llindar 0.7: automatica 53.8%, encert 0.906
llindar 0.8: automatica 45.8%, encert 0.919
Sense etiqueta: {'automatica': 697, 'revisio_humana': 300} | encert real (ocult) automatica: 0.852 | humana: 0.52

El criteri d'èxit de la secció 1 es compleix al prototip: 64,7 % automatitzat amb 85,8 % d'encert en aquesta cua (i 85,2 % a les 997 incidències sense etiqueta, la causa real de les quals coneixem només perquè les dades són sintètiques: és la comprovació que la validació creuada no mentia). La cua humana concentra els casos difícils (51,7 %): l'agent rep la proposta, la confiança i la traça, exactament el que 06-04 demanava. El llindar és un comandament de negoci: 0,7 puja l'encert automàtic al 90,6 % automatitzant el 54 %; el Diego decideix segons la capacitat de l'equip. La incidència I7010 mostra la interacció de regles: el model n'està segur (0,81) però R0 l'envia a revisió pel valor, i tot i així R4 llança el reenviament, perquè reenviar és reversible i barat. En minuts d'agent (2 davant de 8): 1.003 incidències costaven 8.024 min a mà; ara 649 × 2 + 354 × 8 = 4.130, gairebé la meitat, sense comptar la segona incidència per diagnòstic erroni (14 % de 649 ≈ 92 casos), que cal restar i vigilar.

  1. Model card i comprovació de deriva

Recordatori (08-01, seccions 11.3 i 11.5). La model card documenta ús previst i no previst, dades, mètriques i limitacions a partir del que el codi calcula; la comprovació de deriva compara cada lot amb una referència desada en entrenar (PSI per trams, taxes, fracció de cada cua).

La teva tasca. Genera model_card.json i referencia_deriva.json des del codi; escriu comprovar_deriva(lot, referencia) amb PSI de dies_des_enviament, diferències de taxa de símptomes, fracció automàtica i distribució de causes proposades; prova-ho amb un mes normal i amb un mes en què el dany en transport es dispara.

import joblib
card = {"nom": "Diagnostic de causa d'incidencies NovaMarket (cas 9)", "versio": "0.1.0-prototip", "data": "2026-08-18",
        "model": "Regressio logistica multiclasse: magatzem, transportista, 4 simptomes, dies des de l'enviament, valor de la comanda",
        "dades": {"incidencies": int(len(inc)), "etiquetades": int(len(etiq)), "classes": CLASSES},
        "metriques_cv5": {"exactitud": 0.738, "f1_macro": 0.658, "linia_base_xarxa_bayesiana": 0.719, "linia_base_majoritaria": 0.314},
        "cues": {"llindar_automatica": LLINDAR_AUTO, "valor_alt_revisio": VALOR_ALT, "fraccio_automatica": 0.647, "encert_automatica": 0.858},
        "us_previst": "Proposar causa i accions a l'agent; decidir automaticament nomes a la cua automatica i amb accions reversibles",
        "us_no_previst": "Imputar responsabilitat a un transportista o empleat sense revisio humana; fer servir la causa per penalitzar",
        "limitacions": ["fallada_proveidor es detecta malament (recall 0,22)", "nomes 1.003 etiquetes, registrades a la meitat dels casos",
                        "dades sintetiques coherents amb les CPT de 06-03: validar amb dades reals abans del pilot"]}
referencia = {"taxa_simptomes": etiq[SIMPTOMES].mean().round(3).to_dict(), "frac_automatica": 0.647,
              "dist_causa_proposta": etiq2["causa_proposta"].value_counts(normalize=True).round(3).to_dict(),
              "vores_dies": [0, 2, 4, 6, 8, 10, 14, None], "freq_dies": None}
PSI_AVIS, PSI_ALERTA, DIF_TAXA = 0.10, 0.25, 0.08
def psi(ref, nou, eps=1e-4):
    r, n = np.clip(np.asarray(ref, float), eps, None), np.clip(np.asarray(nou, float), eps, None)
    return float(np.sum((n - r) * np.log(n / r)))
def frequencies(valors, vores):
    b = [-np.inf if x is None else x for x in vores]; b[-1] = np.inf
    c, _ = np.histogram(valors, bins=b); return (c / c.sum()).tolist()
referencia["freq_dies"] = frequencies(etiq["dies_des_enviament"], referencia["vores_dies"])
json.dump(card, open("model_card_incidencies.json", "w"), indent=2, ensure_ascii=False)
json.dump(referencia, open("referencia_deriva_incidencies.json", "w"), indent=2); joblib.dump(model, "model_incidencies.joblib")

def comprovar_deriva(lot, referencia, model):
    P = model.predict_proba(lot[COLS]); dec = pd.DataFrame([decidir(f, P[i]) for i, (_, f) in enumerate(lot.iterrows())])
    avisos, v = [], psi(referencia["freq_dies"], frequencies(lot["dies_des_enviament"], referencia["vores_dies"]))
    if v > PSI_AVIS: avisos.append(f"PSI dies = {v:.3f}")
    for s, t in referencia["taxa_simptomes"].items():
        if abs(lot[s].mean() - t) > DIF_TAXA: avisos.append(f"taxa {s}: {lot[s].mean():.3f} davant de {t:.3f}")
    fa = (dec["cua"] == "automatica").mean()
    if abs(fa - referencia["frac_automatica"]) > DIF_TAXA: avisos.append(f"fraccio automatica {fa:.3f} davant de {referencia['frac_automatica']:.3f}")
    dc = dec["causa_proposta"].value_counts(normalize=True)
    for c, t in referencia["dist_causa_proposta"].items():
        if abs(dc.get(c, 0) - t) > DIF_TAXA: avisos.append(f"causa proposta {c}: {dc.get(c, 0):.3f} davant de {t:.3f}")
    estat = "ALERTA" if v > PSI_ALERTA or len(avisos) >= 2 else ("AVIS" if avisos else "OK")
    return estat, avisos
print("Mes normal:", comprovar_deriva(generar_incidencies(400, llavor=7), referencia, model))
p_transport = {"zaragoza": [0.20, 0.55, 0.10, 0.06, 0.09], "getafe": [0.15, 0.45, 0.08, 0.25, 0.07]}   # el dany en transport es dispara
print("Mes amb problema de transport:", comprovar_deriva(generar_incidencies(400, llavor=8, p_causa=p_transport), referencia, model))
Mes normal: ('OK', [])
Mes amb problema de transport: ('ALERTA', ['taxa paquet_danyat: 0.525 davant de 0.259', 'taxa producte_equivocat: 0.150 davant de 0.282', 'taxa no_arriba: 0.177 davant de 0.260', 'fraccio automatica 0.782 davant de 0.647', 'causa proposta error_picking: 0.233 davant de 0.345', 'causa proposta dany_transport: 0.497 davant de 0.240'])

El mes normal passa en verd; el mes amb problema de transport dispara sis avisos coherents entre si (més paquets danyats, més causes de transport proposades, més cua automàtica). Interessant: aquell mes el model encerta més (el dany en transport és la causa més fàcil), així que l'alerta no vol dir "reentrenar" sinó "alguna cosa ha canviat al món: que logística parli amb el transportista". És la diferència entre deriva de dades i de concepte de 08-01, i per això la comprovació la llegeix una persona. Calendari mínim: cada hora, que el lot s'ha executat; cada setmana, aquesta comprovació; cada mes, amb les causes registrades pels agents a la cua humana (que ara es recullen sistemàticament), exactitud real per cua i reentrenament si baixa del 80 % a l'automàtica.

  1. Presentació de resultats al Diego

Recordatori (08-01, secció 9). Començar per la decisió i els diners, una xifra per diapositiva, comparar sempre amb la línia base, dir què no sap el sistema i què es demana.

# Diapositiva Contingut (una idea, una xifra)
1 El problema i la proposta Cada incidència la diagnostica un agent a mà (~8 min). Proposem dues cues: automàtica (2 min, accions reversibles) i humana amb la causa suggerida i la seva explicació.
2 Què hem construït Model entrenat amb les 1.003 incidències amb causa registrada + regles de negoci (valor > 300 €, confiança < 0,6, traça amb transportista, reenviament, avís a compres). Sense dades personals.
3 Resultats davant de la línia base Encerta el 73,8 % (la xarxa de les taules del Diego, 71,9 %; "sempre picking", 31,4 %). A la cua automàtica, 85,8 % d'encert sobre el 64,7 % de les incidències. Estalvi estimat: de 8.024 a ~4.130 minuts d'agent per cada 1.000 incidències.
4 El que no sap i els riscos Fallada de proveïdor es detecta malament (22 %); només la meitat de les incidències tenen causa registrada (demanem que es registri sempre); dades del prototip sintètiques: cal el pilot; cap sanció a transportistes o empleats sense revisió.
5 Decisió que demanem Pilot de 4 setmanes a Zaragoza en mode ombra, llindar 0,6 (o 0,7 si l'equip prefereix 54 % automatitzat amb 90,6 % d'encert), comprovació setmanal de deriva i revisió mensual amb les causes registrades.

  1. Rúbrica final i guió per al cas 4

Torna a la rúbrica de la secció 0 i puntua't amb honestedat; amb la nostra versió, la casella més difícil de justificar és "excel·lent" a Dades (la coherència es va comprovar, però les fuites es van donar per suposades: causa mai no entra al model, i dies_des_enviament es coneix en obrir la incidència, però i si a la realitat es calculés en tancar-la?). Aquesta mena de pregunta és la que t'has de fer a cada fila.

Guió alternatiu, cas 4: classificar ressenyes i prioritzar la resposta d'atenció al client. Mateixa estructura, altres peces:

Pas Què fer
1 Definició Pregunta: per a cada ressenya nova, sentiment i urgència (cal respondre avui)? Decisió: cua "respondre avui" (negativa i urgent: producte trencat, no arriba, cobrament doble), "respondre aquesta setmana", "agrair/automàtic". Mètrica de negoci: ressenyes urgents ateses en < 24 h; tècnica: exactitud del sentiment i recall de la urgència. Línia base: bossa de paraules + logística de 05-05 (0,767) i regles de paraules clau per a la urgència.
2 Dades Amplia les plantilles de ressenyes_nm.py amb una etiqueta urgent per plantilla (les negatives de "va arribar trencat", "no va arribar mai", "em van cobrar dues vegades", "perillós" són urgents; "sorollós" o "car" no) i genera 400 ressenyes amb generar_ressenyes_ampliat de 09-03 retornant plantilla; valida per plantilla.
3 Línia base Regles de paraules clau per a urgència (llista de 10 termes) + bossa de paraules per a sentiment; mesura totes dues per plantilla.
4 Model Dos classificadors (sentiment, urgència) o un de multiclasse de 3 cues; compara bossa de paraules, bigrames i EmbeddingBag de 09-03; F1 de la urgència per damunt de l'exactitud.
5 Combinació Regles sobre les probabilitats: P(urgent) ≥ 0,7 → "avui"; 0,4-0,7 → revisió humana; menció de "cobrament" o "perill" → "avui" sempre (regla dura); respostes d'agraïment automàtiques només si P(positiva) ≥ 0,9.
6 Documentació Model card amb l'advertència que el model no entén negacions ni ironia; deriva: PSI de la longitud de les ressenyes, taxa de negatives, fracció "avui".
7 Presentació Ressenyes urgents ateses en 24 h abans/després; quantes s'escaparien (falsos negatius) i per què; petició: pilot amb revisió humana de la cua "avui" i, si hi ha pressupost, un model preentrenat en català (09-03).

Errors Comuns i Consells

  • Saltar-se la definició i començar pel model. Sense la taula de la secció 1 no sabries que la mètrica que importa és l'encert a la cua automàtica, no l'exactitud global.
  • Avaluar la línia base i el model en files diferents o amb protocols diferents; aquí la xarxa no necessita entrenament i el model sí, però tots dos es mesuren sobre les mateixes 1.003 files i per plec.
  • Fer servir l'etiqueta oculta (causa) per a res que no sigui la comprovació final de la cua sense etiqueta: a la realitat no existeix.
  • Regles que contradiuen el model sense deixar traça. Cada decisió s'ha de poder explicar com una llista de regles disparades amb els seus valors; és el que l'agent i l'auditor llegiran.
  • Deriva = reentrenar. El mes amb problema de transport ensenya que una alerta pot demanar una acció de negoci, no de dades.
  • Documentar al final. La model card es genera des del codi i les xifres surten de les mateixes variables que vas imprimir; si l'escrius a mà al final, es desincronitza.
  • Consell: desa-ho tot com a scripts amb main() i una prova (assert exactitud > 0.70), com a 07-04, i fes servir la rúbrica dues vegades: en començar (què em falta) i en acabar (què he aconseguit).

Conclusió

Amb aquest projecte has recorregut el mètode de 08-01 complet sobre un cas nou: un document de definició amb la decisió (dues cues), la mètrica de negoci (minuts d'agent i encert a la cua automàtica) i les restriccions; un generador d'incidencies.csv coherent amb les taules de 06-03 i amb la meitat de les causes sense etiquetar; la xarxa bayesiana com a línia base (71,9 %, sense etiquetes, amb fallada_proveidor com a punt cec); una logística multiclasse que la supera per 1,9 punts gràcies a dies_des_enviament (73,8 %, F1 macro 0,658), amb la corba d'aprenentatge que mostra quan guanya l'expert i quan les dades; les regles de 06-04 convertint probabilitats en cues (64,7 % automàtic amb 85,8 % d'encert; el llindar com a comandament de negoci); una model card generada des del codi i una comprovació de deriva que distingeix un mes normal d'un amb problema de transport; i cinc diapositives que acaben demanant una decisió. I tens el guió per repetir-ho amb les ressenyes.

Aquí acaba el mòdul 9 i, amb ell, la part pràctica del curs. El que segueix no és més contingut, sinó com continuar aprenent pel teu compte: el mòdul 10 reuneix llibres i articles (10-01), cursos i tutorials (10-02), comunitats i fòrums on preguntar i contribuir (10-03) i, a 10-04, itineraris concrets segons cap a on vulguis anar (enginyeria de dades i ML, deep learning, sistemes basats en coneixement, gestió de projectes d'IA), amb els propers passos que la Marta i el Diego farien a NovaMarket com a exemple.

Fonaments d'Intel·ligència Artificial (IA)

Mòdul 1: Introducció a la Intel·ligència Artificial

Mòdul 2: Principis Bàsics de la IA

Mòdul 3: Algorismes en IA

Mòdul 4: Aprenentatge Automàtic (Machine Learning)

Mòdul 5: Xarxes Neuronals i Deep Learning

Mòdul 6: Lògica i Sistemes Experts

Mòdul 7: Eines i Llenguatges de Programació en IA

Mòdul 8: Projectes i Casos d'Estudi

Mòdul 9: Exercicis i Pràctiques

Mòdul 10: Recursos Addicionals

© Copyright 2026. Tots els drets reservats