El repositori cineclick-churn ja té estructura, llavors fixes i configuració externa, però vam tancar la lliçó anterior assenyalant l'esquerda: res no declara amb quines versions de pandas, scikit-learn o Python s'ha d'executar aquest codi. És el problema núm. 8 del diagnòstic ("dependències implícites"), i en ML és més traïdor que en el programari clàssic, perquè una versió diferent d'una llibreria no sempre trenca amb un error visible: de vegades simplement produeix un model diferent o mètriques diferents, en silenci. En aquesta lliçó muntarem l'entorn reproduïble del projecte: entorn virtual, dependències declarades a pyproject.toml, versions fixades amb un fitxer de lock, i els detalls extra que la reproduïbilitat en ML exigeix més enllà de la llista de paquets.

Contingut

  1. "A la meva màquina funciona", edició ML
  2. Entorns virtuals: un per projecte
  3. Declarar dependències a pyproject.toml
  4. Fixar versions: rangs, pins i fitxers de lock
  5. Reproduïbilitat més enllà dels paquets
  6. Bones pràctiques per al repo de CineClick

"A la meva màquina funciona", edició ML

Imagina que un company nou, Marc, clona cineclick-churn, fa pip install pandas scikit-learn pyyaml al seu Python global i executa l'entrenament. Possibles finals de la història, tots reals:

  • Trencament sorollós: la seva versió de scikit-learn va eliminar o rebatejar un paràmetre que fem servir, i l'script mor amb un TypeError. Molest, però almenys és visible.
  • Trencament silenciós de resultats: entre versions de scikit-learn canvien detalls d'implementació (algorismes interns, gestió d'empats, valors per defecte d'hiperparàmetres). El mateix codi amb la mateixa llavor pot produir un model amb mètriques lleugerament diferents. Marc obté recall 0.41 en lloc de 0.43 i perd una tarda buscant un bug que no existeix: la diferència era la versió de la llibreria. La llavor fixa l'atzar dins d'una versió, no entre versions.
  • El pickle traïdor: Marc intenta carregar el model_churn.pkl entrenat amb una altra versió de scikit-learn. Un pickle no és un format d'intercanvi: és un bolcat d'objectes Python que assumeix que les classes en carregar-lo són les mateixes que en desar-lo. Amb versions diferents pot fallar en carregar, o —pitjor— carregar amb un avís (InconsistentVersionWarning) i predir malament. Regla de ferro: un model picklat se serveix amb exactament les mateixes versions amb què es va entrenar.
  • Contaminació entre projectes: Marc instal·la després un altre projecte que exigeix una versió incompatible de pandas al mateix Python global, i cineclick-churn deixa de funcionar sense que ningú l'hagi tocat.

La conclusió és la mateixa del DevOps clàssic, agreujada: l'entorn és part de l'artefacte. Reproduir el model exigeix reproduir codi + dades + entorn.

Entorns virtuals: un per projecte

La primera defensa és aïllar: cada projecte amb el seu propi conjunt de paquets, sense compartir res amb el Python global ni amb altres projectes. En Python l'eina de sèrie és venv:

# Des de l'arrel de cineclick-churn
python -m venv .venv

# Activar-lo — Linux/macOS:
source .venv/bin/activate
# Activar-lo — Windows (PowerShell):
.venv\Scripts\Activate.ps1

# Comprovar que hi som dins (ha d'apuntar a .venv):
which python        # Get-Command python a PowerShell

Què acaba de passar: python -m venv .venv crea un directori .venv/ amb una còpia lleugera de l'intèrpret i un site-packages buit i propi. En activar-lo, la shell anteposa aquest entorn al PATH: python i pip passen a ser els del projecte. Tot pip install posterior instal·la dins de .venv/, sense tocar res més de la màquina.

Convencions que adoptem a CineClick:

  • L'entorn es diu .venv i viu a l'arrel del projecte (és la convenció que les eines i els editors detecten automàticament).
  • .venv/ va al .gitignore: l'entorn no es versiona (és enorme, específic de cada SO, i regenerable). El que es versiona és la recepta per recrear-lo — d'això van els dos apartats següents.
  • Un projecte = un entorn. No instal·lis mai dependències de dos projectes al mateix entorn "perquè total, són semblants".

Alternativa que veuràs en molts equips de dades: conda, que a més de paquets Python gestiona binaris no-Python (CUDA, compiladors). És una opció legítima, especialment en deep learning; en aquest curs ens quedem amb venv + pip per ser l'estàndard mínim i perquè el nostre stack no ho necessita.

Declarar dependències a pyproject.toml

Amb l'entorn aïllat, falta la recepta. A la lliçó anterior vam deixar un pyproject.toml mínim; ara hi afegim la secció de dependències, distingint dues categories:

  • Dependències de runtime (dependencies): les que el codi necessita per funcionar — pandas, scikit-learn, PyYAML. Qualsevol que instal·li el paquet les necessita, inclosa la imatge de producció.
  • Dependències de desenvolupament (extras opcionals): les que només necessita qui treballa al projecte — pytest, eines de lint o de lock. Producció no n'ha de carregar.
# pyproject.toml
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "cineclick-churn"
version = "0.1.0"
description = "Model de prediccio de churn de CineClick"
requires-python = ">=3.11"
dependencies = [
    "pandas>=2.2,<3",
    "scikit-learn>=1.5,<1.6",
    "pyyaml>=6.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pip-tools>=7.4",
]

[tool.setuptools.packages.find]
where = ["src"]

I la instal·lació per a desenvolupament passa a ser:

pip install -e ".[dev]"

El sufix [dev] activa l'extra: instal·la el paquet en mode editable més les eines de desenvolupament. En producció (o a la imatge Docker del mòdul 4) n'hi haurà prou amb pip install . a seques, sense pytest ni utilitats de desenvolupament a bord.

Fixa't en els rangs: scikit-learn>=1.5,<1.6 és deliberadament més estricte que el de pandas, per allò que vam veure del pickle — no volem que un salt de versió menor de scikit-learn s'esmunyi entre l'entrenament i el servei. La qual cosa ens porta a la pregunta clau: com de fixes han de ser les versions?

Fixar versions: rangs, pins i fitxers de lock

Hi ha tres estratègies possibles, cadascuna amb el seu lloc:

Estratègia Exemple Avantatges Inconvenients Quan fer-la servir
Sense restricció pandas Sempre l'últim Irreproduïble; qualsevol release et pot trencar Mai en un projecte seriós
Rang compatible pandas>=2.2,<3 Flexible; permet pegats de seguretat; evita conflictes entre paquets Dues instal·lacions en dates diferents poden diferir A pyproject.toml (la intenció)
Pin exacte pandas==2.2.3 Reproduïbilitat total Rígid; congela també bugs; conflictes si un altre paquet demana una altra versió Al fitxer de lock (la foto exacta)

La pràctica moderna combina les dues últimes: rangs a pyproject.toml, pins exactes en un fitxer de lock generat automàticament. El pyproject.toml diu "funciono amb qualsevol scikit-learn 1.5.x"; el lock diu "l'entorn beneït, el que produeix exactament les nostres mètriques, és scikit-learn 1.5.2 amb aquestes 30 dependències transitives exactes". L'important del lock és que inclou les dependències transitives: tu declares pandas, però pandas arrossega numpy, python-dateutil, tzdata... i una versió diferent de numpy també pot canviar resultats numèrics.

Per generar el lock farem servir pip-tools (l'alternativa moderna i molt més ràpida és uv, amb un flux gairebé idèntic — uv pip compile / uv pip sync; tria la que prefereixis, el concepte és el mateix):

# Genera requirements.lock a partir de pyproject.toml (resol TOT l'arbre)
pip-compile pyproject.toml --extra dev -o requirements.lock

# Instal·la EXACTAMENT el que diu el lock (i desinstal·la el que sobri)
pip-sync requirements.lock
pip install -e . --no-deps    # el paquet mateix, sense re-resoldre dependències

El requirements.lock resultant és un fitxer de text pla, versionat a Git, amb aquest aspecte (fragment):

# Generat per pip-compile a partir de pyproject.toml — NO editar a mà
numpy==2.1.3
    # via pandas, scikit-learn
pandas==2.2.3
    # via cineclick-churn (pyproject.toml)
scikit-learn==1.5.2
    # via cineclick-churn (pyproject.toml)
...

El flux de treball de l'equip queda així:

flowchart LR
    A["pyproject.toml<br/>(rangs: la intenció)"] -- "pip-compile<br/>(quan canvien deps)" --> B["requirements.lock<br/>(pins: la foto exacta)"]
    B -- "pip-sync<br/>(cada company, cada màquina)" --> C[".venv idèntic<br/>a tot arreu"]
  • Afegir o actualitzar una dependència: s'edita pyproject.toml, es re-executa pip-compile, i es commiteen els dos fitxers junts. El diff del lock mostra exactament quines versions han canviat — revisable en un pull request.
  • Incorporar-se al projecte o desplegar: mai pip install pandas a mà; sempre pip-sync requirements.lock. Marc obté un entorn idèntic al de Laura, byte a byte de versions.
  • Actualitzar deliberadament (p. ex. una versió nova de scikit-learn): pip-compile --upgrade-package scikit-learn, re-entrenar, comparar mètriques, i només aleshores commitejar. L'actualització de dependències en ML és un experiment, no un tràmit.

Reproduïbilitat més enllà dels paquets

Amb entorn + lock cobrim les llibreries, però queden tres serrells que en ML importen:

La versió de Python

Python 3.11 i 3.12 no són intercanviables: canvien rendiment, avisos i, ocasionalment, comportament de llibreries compilades contra cada versió. Ja vam declarar requires-python = ">=3.11" a pyproject.toml (el rang admissible); a més, fixem la versió concreta de desenvolupament en un fitxer .python-version a l'arrel:

3.11.9

És una convenció que entenen els gestors de versions de Python (pyenv, uv, diversos IDE): en entrar al directori, seleccionen aquest intèrpret automàticament. Un fitxer d'una línia que elimina tota una categoria de "a la meva màquina funciona".

PYTHONHASHSEED

Python aleatoritza per defecte el hash de les cadenes a cada procés (una mesura de seguretat). Conseqüència: qualsevol codi el resultat del qual depengui de l'ordre d'iteració d'un set o del hash d'strings pot variar entre execucions encara que totes les llavors de ML estiguin fixades. El nostre pipeline actual no en depèn (pandas i scikit-learn fan servir els seus propis generadors, que ja controlem amb random_state), però és una font clàssica d'irreproduïbilitat difícil de caçar. La vacuna és fixar la variable d'entorn abans d'executar:

# Linux/macOS
export PYTHONHASHSEED=42
# Windows (PowerShell)
$env:PYTHONHASHSEED = "42"

A CineClick la documentem al README i, més endavant, quedarà fixada de sèrie a la imatge Docker (04-03) i als pipelines automatitzats.

Les llavors (ja fetes) i el maquinari (fora d'abast)

Les llavors de train_test_split i del RandomForestClassifier van quedar fixades a 02-01 — són la tercera pota, i la més coneguda, de la reproduïbilitat. Queda un últim nivell que has de conèixer encara que no ens afecti: amb GPU i deep learning, certes operacions són no-deterministes per disseny (paral·lelisme amb sumes en coma flotant en ordre variable), i la reproduïbilitat exacta exigeix opcions específiques del framework, de vegades pagant rendiment. Per al nostre RandomForest en CPU, no aplica; si algun dia CineClick entrena xarxes, recorda que la frontera existeix.

La jerarquia completa, de dins cap enfora:

Capa Què fixa Eina a CineClick
Atzar de l'algorisme Particions i mostreigs random_state=42 (02-01)
Procés Python Hashes d'strings PYTHONHASHSEED=42
Paquets Versions exactes de tot l'arbre requirements.lock (pip-tools)
Intèrpret Versió de Python .python-version + requires-python
Sistema operatiu i llibreries del sistema Tota la resta Docker — el següent nivell d'aïllament, a la lliçó 04-03

Cada capa assumeix l'anterior. I observa l'última fila: encara que entorn virtual + lock cobreixen el 95% dels casos, continuen executant-se sobre el teu sistema operatiu. Empaquetar també el SO és exactament el que fa Docker, i ho deixem per al mòdul 4.

Bones pràctiques per al repo de CineClick

Recapitulant, l'estat del repositori després d'aquesta lliçó:

cineclick-churn/
├── .python-version          # 3.11.9 — versió de Python del projecte
├── .venv/                   # entorn virtual (al .gitignore)
├── pyproject.toml           # paquet + dependències amb rangs (runtime i [dev])
├── requirements.lock        # pins exactes generats amb pip-compile (SÍ que va a Git)
├── ...                      # (la resta igual que a 02-01)

I el ritual d'incorporació de qualsevol company, documentat al README:

git clone <repo> && cd cineclick-churn
python -m venv .venv && source .venv/bin/activate
pip install pip-tools
pip-sync requirements.lock
pip install -e . --no-deps
pytest                        # si això passa, l'entorn està sa

Regles d'equip que queden establertes: ningú no instal·la paquets a mà fora del flux pyproject.toml → pip-compile → commit; el lock només es regenera de manera deliberada i el seu diff es revisa; i tota màquina que entreni o serveixi el model —humana o automatitzada— parteix del mateix lock.

Errors Comuns i Consells

  • Error: versionar .venv/ a Git. És pesat, específic de cada sistema operatiu i regenerable. Es versiona la recepta (pyproject.toml + requirements.lock), mai l'entorn. Si veus .venv/ en un git status, corre cap al .gitignore.
  • Error: pip freeze > requirements.txt com a "lock". Funciona a mitges, però bolca tot el que hi ha instal·lat a l'entorn (inclòs allò que vas provar una tarda i no fas servir), no distingeix directes de transitives, i no es regenera des d'una font d'intenció. pip-compile parteix de pyproject.toml i anota d'on ve cada paquet: és la diferència entre una foto endreçada i el calaix dels cables.
  • Error: entrenar amb unes versions i servir amb unes altres. El cas del pickle traïdor. L'entorn d'entrenament i el d'inferència han de sortir del mateix lock; quan arribem a Docker (04-03) i al servei (04-02), aquesta regla es convertirà en construcció d'imatge, però la disciplina comença avui.
  • Error: actualitzar dependències "perquè hi havia una versió nova" sense re-avaluar. En ML una actualització de scikit-learn pot moure les teves mètriques. Tracta-la com un canvi més: branca, pip-compile --upgrade-package, re-entrenar, comparar mètriques, revisar, mergejar.
  • Consell: si l'equip ja domina uv, fes-lo servir: és un reemplaçament gairebé directe (uv venv, uv pip compile, uv pip sync) i ordres de magnitud més ràpid. L'important d'aquesta lliçó és el patró intenció/lock, no l'eina concreta.

Exercicis

Exercici 1

Marc clona el repo, crea el seu .venv, però en lloc de fer servir el lock executa pip install pandas scikit-learn pyyaml pytest i després pip install -e . --no-deps. Els tests passen, l'entrenament corre sense errors... però obté recall = 0.41 en lloc del 0.43 esperat. (1) Quina és la causa més probable? (2) Per què no ho va detectar cap error ni test? (3) Quines dues ordres hauria d'haver executat en el seu lloc?

Exercici 2

Classifica aquestes dependències del projecte CineClick com a runtime (dependencies) o desenvolupament (optional-dependencies.dev), justificant cadascuna: (a) scikit-learn — entrena i executa el model; (b) pytest — executa els tests; (c) pyyaml — carrega configs/config.yaml; (d) pip-tools — regenera el lock; (e) fastapi — servirà el model al mòdul 4: quan arribi, on anirà?

Exercici 3

El requirements.lock actual fixa scikit-learn==1.5.2. Surt la 1.6.0 amb una millora de rendiment que us interessa. Descriu, pas a pas i amb les ordres concretes, el procediment correcte per adoptar-la a CineClick, incloent-hi què faries amb el model model_churn.pkl ja entrenat i per què.

Solucions

Solució 1: (1) pip install sense versions va instal·lar l'últim de cada paquet, segurament una versió de scikit-learn (o de numpy, transitiva) diferent de la del lock; els detalls interns de l'algorisme canvien entre versions i, encara que la llavor és la mateixa, el model resultant difereix lleugerament. (2) No hi ha error perquè l'API que fem servir no va canviar, i el test de features no avalua mètriques del model — comprova l'encoding, que és determinista en qualsevol versió raonable (els tests que vigilen mètriques arribaran a 05-01). (3) pip-sync requirements.lock seguit de pip install -e . --no-deps: això reprodueix l'entorn beneït exacte.

Solució 2: (a) runtime — sense scikit-learn ni s'entrena ni es pot deserialitzar/fer servir el model; (b) dev — producció no executa tests; (c) runtime — el codi d'entrenament llegeix la configuració en qualsevol entorn on corri; (d) dev — és una eina del flux de treball, el codi no la importa mai; (e) runtime — el servei de predicció la importarà per funcionar en producció (de fet, equips grans de vegades separen extras train/serve per no dur scikit-learn complet al servei i viceversa; per a CineClick, de moment, un sol grup runtime és suficient).

Solució 3: (1) crear una branca (git checkout -b actualitza-sklearn-16); (2) ampliar el rang a pyproject.toml (scikit-learn>=1.5,<1.7 o directament >=1.6,<1.7); (3) regenerar el lock només per a aquest paquet: pip-compile pyproject.toml --extra dev -o requirements.lock --upgrade-package scikit-learn; (4) pip-sync requirements.lock i pip install -e . --no-deps; (5) re-entrenar el model des de zero i comparar les mètriques amb la línia base (accuracy 0.86 / precision 0.61 / recall 0.43 / F1 0.50); (6) descartar el model_churn.pkl antic: un pickle de la 1.5.2 no s'ha de servir des d'un entorn 1.6.0 — el model "bo" passa a ser el re-entrenat; (7) commit de pyproject.toml + requirements.lock junts, pull request amb les mètriques comparades, merge. Si les mètriques empitjoressin sense explicació, es descarta la branca i no ha passat res: aquesta reversibilitat barata és just el que compren el lock i Git.

Conclusió

El problema núm. 8 té els dies comptats: l'entorn de CineClick ja és una recepta versionada —.venv per projecte, dependències amb rangs a pyproject.toml separant runtime de desenvolupament, pins exactes de tot l'arbre a requirements.lock generat amb pip-compile, .python-version i PYTHONHASHSEED tancant els serrells— i qualsevol company pot recrear byte a byte de versions l'entorn que produeix les nostres mètriques. Codi reproduïble (02-01) + entorn reproduïble (aquesta lliçó) ens deixen a les portes del tercer ingredient, i el més voluminós: les dades. El clients_churn.csv continua sent un fitxer solt que cada extracte mensual sobreescriu, sense història ni manera de tornar enrere — el problema núm. 3 del diagnòstic. Git no hi pot (i a la propera lliçó veurem exactament per què), així que toca presentar l'eina nascuda per a això: DVC, el control de versions per a dades.

© Copyright 2026. Tots els drets reservats