IlmHamroh
Data Science va sun'iy intellekt/Loyihalar va karyera6/12-dars43 daqiqa
Mundarija (24)

29.6-dars: GitHub va loyiha tuzilishi

29-QISM — LOYIHALAR VA KARYERA · 6-dars


1. Kirish va motivatsiya

Oldingi darsda musobaqalarni o'rgandik va oxirida bir fikrga keldik: musobaqadan keyin eng qimmatli artefakt — o'rin emas, balki toza repo va writeup. Bu darsda aynan shu repo haqida gaplashamiz: ma'lumotlar fani loyihasi GitHub da qanday ko'rinishi kerak, git ga nima kiradi va nima kirmaydi, README qanday yoziladi, notebookdagi kod qanday qilib sinaladigan modulga aylanadi, va bularning hammasini qanday qilib avtomatik tekshirish mumkin.

Nega bu muhim? Ish beruvchi, hamkasb yoki bir yildan keyingi o'zingiz loyihangizga repo orqali kiradi. Birinchi daqiqada ular README ni o'qiydi, papka tuzilishiga qaraydi va bitta savolga javob izlaydi: "Buni ishga tushirib, natijani qayta ola olamanmi?" 21.11 da PyTorch loyihasini modullarga ajratishni, 27-qismda reproduksiya, paketlash, testlash va CI ni o'rgandik. Endi bularni ko'rsatiladigan shaklga keltiramiz — portfolio 29.7-bob va ish qidirish 29.8-bob uchun poydevor.

Real vaziyat. Junior tahlilchi o'z portfolio loyihasini ochiq GitHub repo ga joyladi: Untitled.ipynb, final_v2_REAL.ipynb, 80 MB li data.csv va config.py — ichida ma'lumotlar bazasi paroli. README da bitta qator: "# project". Bir necha kundan keyin avtomatik skanerlar ochiq repolardagi kalitlarni topib, bazaga ulanishga urinishdi; parolni zudlik bilan almashtirishga to'g'ri keldi. Intervyuda esa suhbatdosh repo ni ochib, 30 soniyadan keyin yopdi: "Bu loyiha nima qilishini tushunmadim." Shu darsning 1-misolida bunday repo ni avtomatik tekshiradigan repo auditori yozamiz — u ushbu muammolarning hammasini bir soniyada topadi.

Bu darsda DS loyihasining repo tuzilishini, git ning ma'lumot va sirlar bilan xavfsiz ishlatilishini, README, testlar va CI ni o'rganamiz — va har birini kod bilan tekshiramiz.

Bu darsda:

  • DS loyiha repo tuzilishi (cookiecutter-data-science g'oyasi)
  • Git ma'lumotlar fani uchun: nima kiradi, nima kirmaydi; .gitignore, sirlar, katta fayllar
  • Yaxshi commit tarixi
  • README anatomiyasi
  • Notebookdan modulga; chiqishlarni tozalash (nbstripout g'oyasi)
  • Testlar (pytest uslubi) va chegara holatlar
  • CI: GitHub Actions
  • pyproject.toml, lint va pre-commit
  • Tuzoqlar

ℹ Misollar real Python bilan (Python 3.14): repo lar, notebooklar va testlar vaqtinchalik papkada yaratiladi. git va pytest mavjud bo'lsa ishlatiladi (tarmoqsiz); bo'lmasa, g'oya o'zimiz yozgan kod bilan ko'rsatiladi. GitHub API bu muhitda yo'q.


2. Nazariya — chuqur tushuntirish

2.1. DS loyiha repo tuzilishi

Ma'lumotlar fanida umumiy qabul qilingan shablonlardan biri — cookiecutter-data-science g'oyasi: har loyihada bir xil papkalar, shunda istalgan kishi istalgan loyihada nima qayerdaligini darhol topadi.

text
mijoz-ketishi/
  README.md             <- birinchi o'qiladigan fayl 2.4-bob
  pyproject.toml        <- paket, qaramliklar, lint sozlamalari 2.8-bob
  .gitignore            <- git ga KIRMAYDIGANLAR 2.2-bob
  .env.example          <- kerakli muhit o'zgaruvchilari NOMLARI, qiymatsiz
  data/
    raw/                <- xom ma'lumot, O'ZGARTIRILMAYDI     } git da EMAS
    interim/            <- oraliq                             } (DVC yoki
    processed/          <- modelga tayyor                     }  tashqi ombor)
  notebooks/
    01-eda.ipynb        <- raqam + qisqa nom; chiqishlar tozalangan
    02-belgilar-tajriba.ipynb
  src/churn/            <- import qilinadigan kod 21.11-bob
    __init__.py
    malumot.py          <- yuklash, tozalash
    belgilar.py         <- belgi yaratish
    train.py            <- kirish nuqtasi: konfig -> ma'lumot -> model -> saqlash
    baholash.py
  configs/
    asos.yaml           <- giperparametrlar, yo'llar (kodda emas)
  tests/
    test_belgilar.py    <- pytest 2.6-bob
  models/               <- o'qitilgan modellar - git da EMAS (registr, 27.5)
  reports/
    figures/            <- hisobot rasmlari (skript bilan qayta yaratiladi)
  .github/workflows/
    ci.yml              <- har push da testlar 2.7-bob

QOIDALAR:
  data/raw - faqat o'qiladi; har o'zgarish kod orqali -> processed
  notebooks - kashfiyot uchun; hisobotdagi raqamlar SKRIPT dan 21.11-bob
  src - notebooklar uni import qiladi, aksincha emas
  models, data - git da emas, ularning versiyasi xesh bilan (27.5, 4-misol)

Bu tuzilish katta loyihalar uchun ham, portfolio uchun ham ishlaydi. Kichik loyihada ba'zi papkalar bo'sh bo'lishi mumkin — muhimi, nomlar va rollar oldindan kelishilgan.

2.2. Git ma'lumotlar fani uchun: nima kiradi, nima kirmaydi

text
GIT GA KIRADI                        GIT GA KIRMAYDI
  kod (src, tests)                     xom va qayta ishlangan ma'lumot
  konfiglar (parolsiz)                 o'qitilgan modellar (.pkl, .pt)
  README, hujjatlar                    .env, kalitlar, tokenlar, parollar
  kichik namuna ma'lumot (tests/       notebook chiqishlari (rasmlar,
    uchun, bir necha KB)                 jadvallar, chop etilgan qiymatlar)
  ma'lumot "ko'rsatkichlari"           __pycache__, .ipynb_checkpoints
    (xesh + hajm, DVC g'oyasi)         katta natija fayllari, loglar
  pyproject.toml / lock fayl           shaxsiy ma'lumot (PII) - hech qachon

Nega ma'lumot git ga kirmaydi? Git har versiyaning to'liq nusxasini saqlaydi: 100 MB li CSV 10 marta o'zgarsa, repo tarixi ~1 GB bo'ladi va uni hech qachon kichraytirib bo'lmaydi (tarixni qayta yozmasdan). GitHub katta fayllarni umuman qabul qilmaydi (aniq chegara — rasmiy hujjatda). Ma'lumot uchun 27.5 dagi DVC g'oyasi ishlatiladi: git da faqat kichik "ko'rsatkich" fayl (xesh, hajm), ma'lumotning o'zi — tashqi omborda. Git LFS — boshqa yondashuv: katta fayllar maxsus serverda, git da ko'rsatkich.

text
.gitignore - DS loyihasi uchun namuna
  data/raw/
  data/interim/
  data/processed/
  models/
  .env
  __pycache__/
  *.pyc
  .ipynb_checkpoints/
  .venv/
  mlruns/
  *.log

Sirlar. Parol, API kaliti, token — faqat muhit o'zgaruvchisida (.env fayl .gitignore da, repo da esa .env.example — faqat nomlar). Agar sir bir marta commit qilingan bo'lsa, uni keyingi commitda o'chirish yetarli emas — u tarixda qoladi. To'g'ri tartib: birinchi navbatda kalitni bekor qilish va yangisini yaratish (rotate), keyin tarixni tozalash. Ochiq repolarni avtomatik skanerlaydigan botlar bor — sir bir necha daqiqada topilishi mumkin.

2.3. Yaxshi commit tarixi

text
YAXSHI COMMIT
  bitta mantiqiy o'zgarish (belgi qo'shildi, xato tuzatildi)
  xabar: <tur>(<soha>): <nima va nega>
    feat: yangi imkoniyat          fix: xato tuzatish
    data: ma'lumot versiyasi       docs: hujjat
    test: testlar                  refactor: xatti-harakatni o'zgartirmaydigan
  misollar:
    feat(belgilar): oxirgi 3 oydagi shikoyatlar soni
    fix(tozalash): manfiy to'lovlar endi 0 ga kesiladi
    data: 2026-08 partiyasi qo'shildi (ko'rsatkich yangilandi)

YOMON COMMIT
  "update", "wip", "asdf", "final final v2"
  50 fayl, 5 xil o'zgarish, bitta xabar
  notebook chiqishlari bilan 3 MB li diff

ISH OQIMI (kichik jamoa yoki yakka)
  main - har doim ishlaydi (testlar o'tadi)
  har vazifa - alohida branch -> pull request -> CI yashil -> merge
  natija (model, hisobot) - commit xeshi bilan bog'lanadi (27.4)

Commit tarixi — sizning ish uslubingizning yozuvi. Portfolio repo sini ko'rgan suhbatdosh "update" lardan iborat 3 ta commitni va mantiqiy qadamlar bilan qurilgan 40 ta commitni farqlaydi.

2.4. README anatomiyasi

README — repo ning "birinchi sahifasi". U teskari piramida tamoyili bilan yoziladi 29.4-bob: eng muhimi — tepada.

text
README TUZILISHI
  1. Sarlavha + BIR GAPLIK xulosa (nima, kim uchun, asosiy natija raqami)
  2. Natija: asosiy jadval yoki rasm; bazaviy model bilan taqqoslash
  3. Muammo: nima uchun bu savol muhim
  4. Ma'lumot: manba, hajm, davr, litsenziya; git da emasligi va qanday olish
  5. O'rnatish: 2-3 buyruq
  6. Foydalanish: qayta ishga tushirish buyrug'i (train, baholash)
  7. Tuzilish: papkalar qisqacha
  8. Cheklovlar: nima tekshirilmagan, qayerda ishlamaydi
  9. Litsenziya, muallif, bog'lanish
markdown
# Mijoz ketishini bashorat qilish

Telekom mijozlarining 3 oy ichida ketishini bashorat qiluvchi model:
test AUC 0.81 (bazaviy logistik regressiya 0.72), top-10% xavfli
mijozlar ketuvchilarning 34% ini qamraydi. *(Raqamlar - namuna.)*

## Natija
| model              | test AUC | 95% CI       |
|--------------------|----------|--------------|
| bazaviy (logreg)   | 0.72     | [0.70, 0.74] |
| HistGB (yakuniy)   | 0.81     | [0.79, 0.83] |

## Muammo
Saqlash bo'limi oyiga cheklangan miqdorda taklif yubora oladi ...

## Ma'lumot
Sintetik, 20 000 mijoz, 12 oy. `data/` git da emas:
`python -m churn.malumot --yarat` bilan qayta yaratiladi.

## O'rnatish
    pip install -e .

## Foydalanish
    python -m churn.train --config configs/asos.yaml
    python -m churn.baholash --model models/histgb.pkl

## Tuzilish
`src/churn` - kod, `tests` - testlar, `notebooks` - tahlil, `configs` - sozlamalar

## Cheklovlar
Bitta hudud ma'lumoti; model kalibrlanmagan; vaqt bo'yicha drift tekshirilmagan.

Birinchi ekran qoidasi: README ning birinchi ekranida (taxminan 10-15 qator) loyiha nima qilishi va asosiy natija raqami bo'lishi kerak. 29.7 da buni "30 soniyalik ko'rib chiqish" simulyatsiyasi bilan o'lchaymiz.

2.5. Notebookdan modulga

21.11 da amaliy tartibni ko'rgan edik: notebookda sinash — ishlagan kodni modulga ko'chirish — notebook modulni import qiladi. Portfolio repo uchun yana ikki narsa muhim:

text
NOTEBOOK FAYLI (.ipynb) - bu JSON:
  cells: [ {cell_type, source, outputs, execution_count, metadata}, ... ]
  outputs ichida: jadvallar (HTML), rasmlar (base64 PNG), chop etilgan matn

MUAMMOLAR
  hajm:   bitta rasm - yuzlab KB; repo tez shishadi
  diff:   kod o'zgarmasa ham, qayta ishga tushirish ijro raqamlari va
          rasmlarni o'zgartiradi -> git diff da yuzlab "o'zgarish"
  sir:    print(parol), df.head() da shaxsiy ma'lumot -> chiqishda qoladi

YECHIMLAR
  nbstripout g'oyasi: commit dan oldin outputs = [], execution_count = None
    (git filtri yoki pre-commit ilovasi sifatida ishlatiladi)
  jupytext g'oyasi: notebook bilan birga .py matn nusxasi - diff o'qiladi
  kodni src/ ga ko'chirish: funksiyalar, konstantalar, importlar;
    magiclar (%matplotlib, !pip) va ko'rsatish qatorlari (df.head()) - tashlanadi

2.6. Testlar (pytest uslubi)

27.8 da API testlarini o'rgandik. DS loyihasida testlar kodning mantiqini himoya qiladi — model sifatini emas (u baholash skriptida):

text
NIMANI TEST QILISH KERAK
  tozalash va belgi funksiyalari - kichik qo'lda yozilgan misollar
  CHEGARA HOLATLAR - bo'sh ro'yxat, nol, NaN, chegara qiymat (29, 30),
                     noto'g'ri format, bitta sinf
  xossalar - "F1 har doim [0, 1] da", "bo'lishda train va test kesishmaydi"
  sxema - ustunlar, turlar, diapazonlar 27.3-bob
  golden - kichik kirish -> ma'lum chiqish 27.8-bob
  tutunli (smoke) test - butun quvur 100 qatorda xatosiz ishlaydimi

PYTEST QOIDALARI
  fayl: tests/test_*.py, funksiya: test_*
  oddiy assert; istisno uchun pytest.raises
  parametrlash: @pytest.mark.parametrize
  ishga tushirish: python -m pytest -q
python
import pytest

from churn.belgilar import yosh_guruhi


@pytest.mark.parametrize("yosh, kutilgan", [(29, "18-29"), (30, "30-44"),
                                            (44, "30-44"), (45, "45+")])
def test_yosh_chegaralar(yosh, kutilgan):
    assert yosh_guruhi(yosh) == kutilgan


def test_telefon_notogri():
    with pytest.raises(ValueError):
        telefon_tozala("12-34")

Oddiy holat testlari xatoni kamdan-kam ushlaydi — xatolar chegarada yashaydi. 3-misolda buni o'lchaymiz.

2.7. CI: GitHub Actions

CI (continuous integration) — har push va pull request da testlarni avtomatik ishga tushirish. GitHub da bu .github/workflows/ papkasidagi YAML fayl:

yaml
# .github/workflows/ci.yml
name: ci
on:
  push:
    branches: [main]
  pull_request:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
      - run: pip install -e ".[dev]"
      - run: ruff check .                    # lint (2.8)
      - run: python -m pytest -q             # testlar
      - run: python -m churn.train --config configs/kichik.yaml   # tutunli test
text
CI DA NIMALAR BO'LADI
  lint -> testlar -> kichik ma'lumotda tutunli o'qitish
  sirlar kerak bo'lsa: GitHub "Secrets" orqali muhit o'zgaruvchisi
  katta ma'lumot / GPU - CI da emas (alohida quvur, 27.14 CT)
README da "CI: passing" belgisi - repo sog'lom ekanining tez signali

2.8. pyproject.toml, lint va pre-commit

toml
[project]
name = "churn"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "numpy==2.3.2",
    "pandas==3.0.1",
    "scikit-learn==1.9.0",
]

[project.optional-dependencies]
dev = ["pytest", "ruff"]

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I"]     # xatolar, ishlatilmagan import, import tartibi

[tool.pytest.ini_options]
testpaths = ["tests"]

(Versiya raqamlari — namuna; o'z muhitingizdagi versiyalarni qading, 27.2.)

yaml
# .pre-commit-config.yaml - commit dan OLDIN avtomatik tekshiruvlar
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.0            # namuna versiya
    hooks:
      - id: ruff
  - repo: https://github.com/kynan/nbstripout
    rev: 0.7.1             # namuna versiya
    hooks:
      - id: nbstripout     # notebook chiqishlarini tozalash

Lint — kod uslubi va oddiy xatolarni (ishlatilmagan import, aniqlanmagan nom) avtomatik topish. Portfolio repo da lint toza bo'lishi — "bu odam jamoada ishlay oladi" degan signal.

2.9. Litsenziya va ma'lumot huquqlari

Ochiq repo ga LICENSE fayl qo'shing (masalan, MIT yoki Apache-2.0 — farqini rasmiy manbalardan o'qing). Litsenziyasiz kodni boshqalar huquqan qayta ishlata olmaydi. Ma'lumot uchun alohida savol: siz ishlatgan ma'lumotni qayta tarqatish mumkinmi? Musobaqa ma'lumotlari ko'pincha qayta tarqatilmaydi — README da manbaga havola bering, faylni emas. Ish joyidagi ma'lumotni portfolio ga olib chiqish — hech qachon 29.11-bob.

2.10. Tuzoqlar

Asosiy tuzoqlar: ma'lumot va modellarni git ga qo'shish; .env va kalitlarni commit qilish, keyin "o'chirib" qo'yish bilan cheklanish (kalit tarixda qoladi — avval rotate); .gitignore ni loyiha oxirida yozish (fayl allaqachon kuzatilmoqda); notebookni chiqishlari bilan commit qilish; "Untitled.ipynb", "final_v2" kabi nomlar; README da natija yo'q yoki oxirida; o'rnatish buyruqlari ishlamaydi; qaramlik versiyalari qadalmagan; testlar faqat oddiy holatlarni tekshiradi; CI yo'q yoki qizil holda qoldirilgan; "update" kabi commit xabarlari; notebook src ni emas, src notebookdan nusxa ko'chiradi; shaxsiy yoki ish joyidagi ma'lumotni ochiq repo ga joylash.


3. Tez ma'lumotnoma

python
import hashlib
import json
import re

# sir qidirish (soddalashtirilgan)
SIR = re.compile(r"(?i)(api[_-]?key|secret|token|password|parol)\s*[:=]\s*['\"]?"
                 r"([A-Za-z0-9_\-+/]{12,})")
topildi = [i for i, q in enumerate(matn.splitlines(), 1) if SIR.search(q)]

# notebook chiqishlarini tozalash (nbstripout g'oyasi)
nb = json.loads(open("01-eda.ipynb", encoding="utf-8").read())
for k in nb["cells"]:
    if k["cell_type"] == "code":
        k["outputs"], k["execution_count"] = [], None
    k["metadata"] = {}

# git blob xeshi (tarkibga bog'liq, vaqtga emas)
xesh = hashlib.sha1(b"blob %d\x00" % len(baytlar) + baytlar).hexdigest()

# ma'lumot ko'rsatkichi (DVC g'oyasi)
korsatkich = {"yol": "data/raw/mijozlar.csv",
              "md5": hashlib.md5(baytlar).hexdigest(), "hajm": len(baytlar)}
bash
git init
git add -A && git status --short          # nima kuzatilmoqda?
git check-ignore -v data/raw/mijozlar.csv # qaysi qoida e'tiborsiz qildi
git rm -r --cached data/                  # tasodifan qo'shilgan papkani kuzatuvdan chiqarish
git log --oneline -10
python -m pytest -q
ruff check .

Qaysi vaziyatda nima

Vaziyat Nima qilish kerak
Yangi loyiha Shablon tuzilish + .gitignore BIRINCHI commitda
Katta ma'lumot Git da emas; ko'rsatkich fayl (DVC) yoki LFS
Kalit commit qilindi Darhol rotate, keyin tarixni tozalash
Notebook commit Chiqishlarni tozalash (nbstripout, pre-commit)
Funksiya yozildi Chegara holat testlari bilan birga
Repo ochiq qilinadi Auditor: README, sirlar, katta fayllar, testlar, litsenziya

Repo xulosasi

tuzilish: data (git da emas) / notebooks / src / tests / configs / models (git da emas)
.gitignore birinchi commitda; sir -> .env; commit qilingan sir -> rotate
README: bir gaplik xulosa + natija raqami tepada; o'rnatish va foydalanish
notebook: chiqishsiz; kod src da; notebook src ni import qiladi
testlar: chegara holatlar; CI: lint + pytest + tutunli test

4. Batafsil misollar

Misollar real Python bilan (Python 3.14). Har misol mustaqil ishlaydi va hamma fayllarni vaqtinchalik papkada yaratadi; git va pytest mavjudligi tekshiriladi.

Misol 1 — Repo auditori: yomon va yaxshi repo, sirlar va ball

python
"""Repo auditori: DS loyiha reposini avtomatik tekshirish va ball qo'yish."""

import fnmatch
import json
import math
import re
import tempfile
from collections import Counter
from pathlib import Path

import numpy as np

README_BOLIMLAR = ["muammo", "natija", "ma'lumot", "o'rnatish", "foydalanish",
                   "tuzilish", "cheklov"]
SIR_REGEX = re.compile(
    r"(?i)(api[_-]?key|secret|token|password|parol|kalit)\s*[:=]\s*['\"]?"
    r"([A-Za-z0-9_\-+/]{12,})")
OGIRLIK = {"README": 20, ".gitignore": 15, "sirlar yo'q": 25, "katta fayl yo'q": 10,
           "testlar": 10, "qaramliklar": 10, "notebook toza": 5, "CI": 5}
KATTA = 1_000_000


def yoz(ildiz, nisbiy, matn):
    p = ildiz / nisbiy
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(matn, encoding="utf-8", newline="\n")


def notebook(chiqishli):
    kat = {"cell_type": "code", "execution_count": 3 if chiqishli else None,
           "metadata": {}, "source": ["df.describe()"], "outputs": []}
    if chiqishli:
        kat["outputs"] = [{"output_type": "execute_result", "execution_count": 3,
                           "metadata": {}, "data": {"text/plain": ["x" * 80] * 3000}}]
    return json.dumps({"cells": [kat], "metadata": {}, "nbformat": 4,
                       "nbformat_minor": 5}, indent=1)


def yomon_repo(ildiz, rng):
    kalit = "".join(rng.choice(list("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"), 32))
    yoz(ildiz, "README.md", "# loyiha\n\nmodel\n")
    yoz(ildiz, "Untitled.ipynb", notebook(True))
    yoz(ildiz, "final_v2_REAL.ipynb", notebook(True))
    yoz(ildiz, "config.py", f'API_KEY = "{kalit}"\nDB = "prod"\n')
    yoz(ildiz, ".env", f"DB_PAROL={kalit[::-1]}\n")
    yoz(ildiz, "requirements.txt", "pandas\nscikit-learn\n")
    qatorlar = "\n".join(f"{i},{rng.integers(18, 70)},{rng.random():.6f}"
                         for i in range(60_000))
    yoz(ildiz, "data.csv", "id,yosh,ball\n" + qatorlar + "\n")


def yaxshi_repo(ildiz, rng):
    yoz(ildiz, "README.md", "\n".join([
        "# Mijoz ketishini bashorat qilish",
        "Telekom mijozlarining 3 oy ichida ketishini bashorat qiluvchi model; "
        "test AUC 0.81 (bazaviy 0.72).",
        "## Muammo", "Saqlash bo'limi kimga taklif yuborishni bilishi kerak.",
        "## Natija", "| model | AUC |", "|---|---|", "| HistGB | 0.81 |",
        "## Ma'lumot", "Sintetik, 20 000 mijoz; data/ git da emas.",
        "## O'rnatish", "    pip install -e .",
        "## Foydalanish", "    python -m churn.train --config configs/asos.yaml",
        "## Tuzilish", "src/, tests/, configs/, notebooks/",
        "## Cheklovlar", "Faqat bitta hudud ma'lumoti; kalibrlash tekshirilmagan.", ""]))
    yoz(ildiz, ".gitignore", "data/\n.env\nmodels/\n__pycache__/\n*.pyc\n"
                             ".ipynb_checkpoints/\n")
    yoz(ildiz, ".env.example", "DB_PAROL=<bu yerga o'z parolingiz>\n")
    yoz(ildiz, "pyproject.toml", '[project]\nname = "churn"\nversion = "0.1.0"\n'
        'dependencies = ["pandas==3.0.1", "scikit-learn==1.9.0"]\n')
    yoz(ildiz, "src/churn/__init__.py", "")
    yoz(ildiz, "src/churn/belgilar.py", "def yil(oy):\n    return oy / 12\n")
    yoz(ildiz, "tests/test_belgilar.py",
        "from churn.belgilar import yil\n\n\ndef test_yil():\n    assert yil(24) == 2\n")
    yoz(ildiz, "configs/asos.yaml", "model: histgb\nseed: 0\n")
    yoz(ildiz, "notebooks/01-eda.ipynb", notebook(False))
    yoz(ildiz, ".github/workflows/ci.yml", "name: ci\non: [push]\n")
    qatorlar = "\n".join(f"{i},{rng.integers(18, 70)}" for i in range(120_000))
    yoz(ildiz, "data/raw/mijozlar.csv", "id,yosh\n" + qatorlar + "\n")


def gitignore_qoidalari(ildiz):
    p = ildiz / ".gitignore"
    if not p.exists():
        return []
    return [q.strip() for q in p.read_text(encoding="utf-8").splitlines()
            if q.strip() and not q.startswith("#")]


def etiborsiz(nisbiy, qoidalar):
    """.gitignore ning soddalashtirilgan talqini: papka/, *.kengaytma, aniq nom."""
    qismlar = nisbiy.split("/")
    for q in qoidalar:
        if q.endswith("/"):
            if q.rstrip("/") in qismlar[:-1]:
                return True
        elif any(fnmatch.fnmatch(k, q) for k in qismlar):
            return True
    return False


def entropiya(s):
    n = len(s)
    return -sum(c / n * math.log2(c / n) for c in Counter(s).values())


def yaxlit_token(s):
    """Sir odatda bo'shliqsiz, vergulsiz uzun token bo'ladi."""
    return re.fullmatch(r"[A-Za-z0-9_\-+/=]{24,}", s) is not None


def audit(ildiz):
    qoidalar = gitignore_qoidalari(ildiz)
    fayllar = sorted(p.relative_to(ildiz).as_posix() for p in ildiz.rglob("*") if p.is_file())
    kuzatiladi = [f for f in fayllar if not etiborsiz(f, qoidalar)]
    ball, muammo = {}, []

    readme = ildiz / "README.md"
    matn = readme.read_text(encoding="utf-8").lower() if readme.exists() else ""
    sarlavhalar = " ".join(re.findall(r"^#+\s*(.+)$", matn, flags=re.M))
    bor = [b for b in README_BOLIMLAR if b in sarlavhalar]
    ball["README"] = len(bor) / len(README_BOLIMLAR)
    if len(bor) < len(README_BOLIMLAR):
        muammo.append("README: yetishmaydi: " + ", ".join(
            b for b in README_BOLIMLAR if b not in bor))

    kerak = ["data/", ".env", "models/"]
    ball[".gitignore"] = sum(k in qoidalar for k in kerak) / len(kerak)
    if ball[".gitignore"] < 1:
        muammo.append(".gitignore: yo'q qoidalar: " + ", ".join(
            k for k in kerak if k not in qoidalar))

    sirlar = []
    for f in kuzatiladi:
        if f.endswith(".ipynb") or f.endswith(".csv"):
            continue
        for i, q in enumerate((ildiz / f).read_text(encoding="utf-8").splitlines(), 1):
            m = SIR_REGEX.search(q)
            if m and entropiya(m.group(2)) > 3.5:
                sirlar.append(f"{f}:{i}")
            elif not m and "=" in q:
                qiymat = q.split("=", 1)[1].strip().strip("'\"")
                if yaxlit_token(qiymat) and entropiya(qiymat) > 4.0:
                    sirlar.append(f"{f}:{i} (yuqori entropiya)")
    ball["sirlar yo'q"] = 0.0 if sirlar else 1.0
    muammo += [f"SIR: {s}" for s in sirlar]

    katta = [f for f in kuzatiladi if (ildiz / f).stat().st_size > KATTA]
    ball["katta fayl yo'q"] = 0.0 if katta else 1.0
    muammo += [f"katta fayl git da: {f} ({(ildiz / f).stat().st_size / 1e6:.1f} MB)"
               for f in katta]

    testlar = [f for f in kuzatiladi if re.search(r"(^|/)test_\w+\.py$", f)
               and "def test_" in (ildiz / f).read_text(encoding="utf-8")]
    ball["testlar"] = 1.0 if testlar else 0.0
    if not testlar:
        muammo.append("testlar yo'q")

    qaram = ""
    for f in ["pyproject.toml", "requirements.txt"]:
        if (ildiz / f).exists():
            qaram += (ildiz / f).read_text(encoding="utf-8")
    qadalgan = "==" in qaram
    ball["qaramliklar"] = 1.0 if qadalgan else (0.3 if qaram else 0.0)
    if not qadalgan:
        muammo.append("qaramlik versiyalari qadalmagan (==)")

    chiqishli = [f for f in kuzatiladi if f.endswith(".ipynb") and any(
        k.get("outputs") for k in json.loads((ildiz / f).read_text(encoding="utf-8"))["cells"])]
    ball["notebook toza"] = 0.0 if chiqishli else 1.0
    muammo += [f"notebook chiqishlari bilan: {f}" for f in chiqishli]

    ball["CI"] = 1.0 if any(f.startswith(".github/workflows/") for f in kuzatiladi) else 0.0
    if not ball["CI"]:
        muammo.append("CI yo'q")
    jami = sum(OGIRLIK[k] * ball[k] for k in OGIRLIK)
    return jami, ball, muammo, len(fayllar), len(kuzatiladi)


def chop(nom, natija):
    jami, ball, muammo, n_f, n_k = natija
    print(f"  {nom}: {n_f} fayl, git kuzatadi {n_k}; BALL {jami:.0f}/100")
    for k in OGIRLIK:
        print(f"    {k:<16} {ball[k]:.2f} x {OGIRLIK[k]:>2}")
    for m in muammo:
        print(f"    - {m}")


def main() -> None:
    rng = np.random.default_rng(0)
    with tempfile.TemporaryDirectory() as tmp:
        tmp = Path(tmp)
        yomon, yaxshi = tmp / "yomon", tmp / "yaxshi"
        yomon_repo(yomon, rng)
        yaxshi_repo(yaxshi, rng)

        print("=== 1. Yomon repo ===")
        n1 = audit(yomon)
        chop("yomon", n1)
        print("\n=== 2. Yaxshi repo ===")
        n2 = audit(yaxshi)
        chop("yaxshi", n2)

        print("\n=== 3. Yomon repoga uchta tez tuzatish ===")
        yoz(yomon, ".gitignore", "data/\n.env\nmodels/\n*.csv\n")
        (yomon / "config.py").write_text(
            'import os\nAPI_KEY = os.environ["API_KEY"]\nDB = "prod"\n', encoding="utf-8")
        yoz(yomon, "requirements.txt", "pandas==3.0.1\nscikit-learn==1.9.0\n")
        n3 = audit(yomon)
        print(f"  ball: {n1[0]:.0f} -> {n3[0]:.0f}; muammolar: {len(n1[2])} -> {len(n3[2])}")
        for m in n3[2]:
            print(f"    qoldi: {m}")

        print("\n=== 4. Sir detektori: to'g'ri va noto'g'ri signallar ===")
        tekshir = [
            ("token = " + "".join(rng.choice(list("abcdef0123456789"), 40)), True),
            ('parol = "<o_zgartiring>"', False),
            ('password = "aaaaaaaaaaaaaaaa"', False),
            ("API_KEY = os.environ['API_KEY']", False),
            ("url = " + "".join(rng.choice(list("ABCDEFGHJKabcdefghjk0123456789+/"), 44)), True),
            ("model_nomi = histgb_baza_versiya_birinchi", False),
            ('dependencies = ["pandas==3.0.1", "scikit-learn==1.9.0"]', False),
            ('parol = "Qwerty123"', True),
        ]
        togri = 0
        for q, kutilgan in tekshir:
            m = SIR_REGEX.search(q)
            topdi = bool(m and entropiya(m.group(2)) > 3.5)
            if not m and "=" in q:
                v = q.split("=", 1)[1].strip().strip("'\"")
                topdi = yaxlit_token(v) and entropiya(v) > 4.0
            togri += topdi == kutilgan
            print(f"    {'SIR' if topdi else '---'}  kutilgan {'SIR' if kutilgan else '---'}  "
                  f"{q.split('=')[0].strip()}")
        print(f"  to'g'ri: {togri}/{len(tekshir)}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Yomon repo ===
  yomon: 7 fayl, git kuzatadi 7; BALL 3/100
    README           0.00 x 20
    .gitignore       0.00 x 15
    sirlar yo'q      0.00 x 25
    katta fayl yo'q  0.00 x 10
    testlar          0.00 x 10
    qaramliklar      0.30 x 10
    notebook toza    0.00 x  5
    CI               0.00 x  5
    - README: yetishmaydi: muammo, natija, ma'lumot, o'rnatish, foydalanish, tuzilish, cheklov
    - .gitignore: yo'q qoidalar: data/, .env, models/
    - SIR: .env:1
    - SIR: config.py:1
    - katta fayl git da: data.csv (1.1 MB)
    - testlar yo'q
    - qaramlik versiyalari qadalmagan (==)
    - notebook chiqishlari bilan: Untitled.ipynb
    - notebook chiqishlari bilan: final_v2_REAL.ipynb
    - CI yo'q

=== 2. Yaxshi repo ===
  yaxshi: 11 fayl, git kuzatadi 10; BALL 100/100
    README           1.00 x 20
    .gitignore       1.00 x 15
    sirlar yo'q      1.00 x 25
    katta fayl yo'q  1.00 x 10
    testlar          1.00 x 10
    qaramliklar      1.00 x 10
    notebook toza    1.00 x  5
    CI               1.00 x  5

=== 3. Yomon repoga uchta tez tuzatish ===
  ball: 3 -> 60; muammolar: 10 -> 5
    qoldi: README: yetishmaydi: muammo, natija, ma'lumot, o'rnatish, foydalanish, tuzilish, cheklov
    qoldi: testlar yo'q
    qoldi: notebook chiqishlari bilan: Untitled.ipynb
    qoldi: notebook chiqishlari bilan: final_v2_REAL.ipynb
    qoldi: CI yo'q

=== 4. Sir detektori: to'g'ri va noto'g'ri signallar ===
    SIR  kutilgan SIR  token
    ---  kutilgan ---  parol
    ---  kutilgan ---  password
    ---  kutilgan ---  API_KEY
    SIR  kutilgan SIR  url
    ---  kutilgan ---  model_nomi
    ---  kutilgan ---  dependencies
    ---  kutilgan SIR  parol
  to'g'ri: 7/8

Natija tahlili.

1-bo'lim — yomon repo. Auditor 7 faylning hammasini "git kuzatadi" deb hisobladi — .gitignore yo'q, shuning uchun .env va 1.1 MB li data.csv ham repo ga tushadi. Ball 3/100: faqat requirements.txt borligi uchun qisman ball (versiyalar qadalmagan — 0.30). Muammolar ro'yxati — kirish qismidagi real vaziyatning aynan o'zi: README da birorta bo'lim yo'q, ikkita sir (.env:1 va config.py:1), katta fayl, testlar va CI yo'q, ikkita notebook chiqishlari bilan. Auditor har muammo uchun fayl va qatorni ko'rsatadi (yo'l — repo ichidagi nisbiy nom), shuning uchun uni tuzatish oson.

2-bo'lim — yaxshi repo 100/100. E'tibor bering: papkada 11 fayl, git esa 10 tasini kuzatadi — 1.1 MB li data/raw/mijozlar.csv .gitignore dagi data/ qoidasi bilan chiqarib tashlangan, shuning uchun u "katta fayl" muammosi emas. .env.example dagi <bu yerga o'z parolingiz> sir sifatida belgilanmadi — unda bo'shliq va burchak qavslar bor, u "yaxlit token" emas.

3-bo'lim — uchta tez tuzatish (.gitignore, kalitni os.environ ga ko'chirish, versiyalarni qadash) ballni 3 dan 60 ga ko'tardi, muammolar 10 dan 5 ga tushdi. Qolganlari — ko'proq mehnat talab qiladigan ishlar: README yozish, testlar, notebooklarni tozalash, CI. Muhim ogohlantirish: bu "tuzatish" faqat hozirgi holatni tuzatadi. Agar yomon repo allaqachon ochiq joylangan bo'lsa, kalit git tarixida qoladi — avval kalitni bekor qilish kerak 2.2-bob.

4-bo'lim — sir detektorining halol bahosi: 7/8 to'g'ri. U 40 belgili hex tokenni va kalit so'zsiz, lekin yuqori entropiyali 44 belgili qatorni topdi; <o_zgartiring> kabi shablonni, aaaa... kabi past entropiyali qiymatni, os.environ ni va oddiy uzun nomni to'g'ri o'tkazib yubordi. dependencies = [...] qatori ham o'tkazib yuborildi — bu tasodif emas: detektorning birinchi versiyasi aynan shu qatorni "yuqori entropiya" deb belgilagan edi (noto'g'ri signal), shundan keyin "sir — bo'shliqsiz yaxlit token" qoidasi qo'shildi. Lekin parol = "Qwerty123" — o'tkazib yuborildi: 9 belgi, regex talab qiladigan 12 dan qisqa. Har qanday skaner — bu tarmoq, devor emas: asosiy himoya — sirni umuman kodga yozmaslik.

Misol 2 — Notebookdan modulga: chiqishlarni tozalash, diff shovqini va sir sizishi

python
"""Notebookdan modulga: kod kataklarini ajratish va chiqishlarni tozalash."""

import ast
import base64
import difflib
import json
import re
import tempfile
from pathlib import Path

import numpy as np
import pandas as pd

SIR_REGEX = re.compile(r"(?i)(parol|password|token|api[_-]?key)\s*[:=]\s*\S{8,}")


def kod(manba, chiqish=None, n=None):
    return {"cell_type": "code", "execution_count": n, "metadata": {"ExecuteTime": {}},
            "source": manba.splitlines(keepends=True), "outputs": chiqish or []}


def notebook_yarat(rng, qayta=0):
    """EDA notebook: magiclar, ko'rsatish qatorlari, rasm va sir sizgan chiqish.
    qayta - notebook qayta ishga tushirilgan (ijro raqamlari va rasm o'zgaradi)."""
    parol = "".join(rng.choice(list("abcdefghkmnpqrstuvwxyz23456789"), 16))
    rasm = base64.b64encode(rng.integers(0, 256, 150_000, dtype=np.uint8).tobytes()).decode()
    jadval = "<table>" + "".join(f"<tr><td>{i}</td><td>{i * 3}</td></tr>"
                                 for i in range(400)) + "</table>"
    q = 10 * qayta
    kataklar = [
        {"cell_type": "markdown", "metadata": {}, "source": ["# Mijoz ketishi - EDA\n"]},
        kod("import numpy as np\nimport pandas as pd\n%matplotlib inline\n", n=1 + q),
        kod("!pip install lightgbm\n", n=2 + q,
            chiqish=[{"output_type": "stream", "name": "stdout",
                      "text": ["Requirement already satisfied\n"] * 30}]),
        kod("import os\nprint('parol:', os.environ['DB_PAROL'])\n", n=3 + q,
            chiqish=[{"output_type": "stream", "name": "stdout",
                      "text": [f"parol: {parol}\n"]}]),
        kod("KECHIKISH_CHEGARASI = 30\n\n\n"
            "def tozala(df):\n"
            "    df = df.drop_duplicates()\n"
            "    df['tolov'] = df['tolov'].clip(lower=0)\n"
            "    return df\n", n=4 + q),
        kod("df = pd.DataFrame({'tolov': [100.0, -5.0, 100.0], 'oy': [12, 30, 12]})\n"
            "df = tozala(df)\ndf.head()\n", n=5 + q,
            chiqish=[{"output_type": "execute_result", "execution_count": 5 + q,
                      "metadata": {}, "data": {"text/html": [jadval]}}]),
        kod("def belgi_qur(df):\n"
            "    df = df.copy()\n"
            "    df['yil'] = df['oy'] / 12\n"
            "    df['kechikkan'] = df['oy'] > KECHIKISH_CHEGARASI\n"
            "    return df\n", n=6 + q),
        kod("df2 = belgi_qur(df)\ndf2.plot()\n", n=7 + q,
            chiqish=[{"output_type": "display_data", "metadata": {},
                      "data": {"image/png": rasm}}]),
    ]
    return {"cells": kataklar, "metadata": {"kernelspec": {"name": "python3"}},
            "nbformat": 4, "nbformat_minor": 5}


def tozala_notebook(nb):
    """nbstripout g'oyasi: chiqishlar, ijro raqamlari va shovqinli metadata o'chadi."""
    nb = json.loads(json.dumps(nb))
    for k in nb["cells"]:
        if k["cell_type"] == "code":
            k["outputs"] = []
            k["execution_count"] = None
        k["metadata"] = {}
    return nb


def modulga(nb):
    """Kod kataklaridan importlar, konstantalar va funksiyalarni ajratadi."""
    qismlar, tashlandi = [], {"magic": 0, "skript": 0}
    for k in nb["cells"]:
        if k["cell_type"] != "code":
            continue
        qatorlar = []
        for q in "".join(k["source"]).splitlines():
            if q.lstrip().startswith(("%", "!")):
                tashlandi["magic"] += 1
            else:
                qatorlar.append(q)
        for tugun in ast.parse("\n".join(qatorlar)).body:
            konst = (isinstance(tugun, ast.Assign) and all(
                isinstance(t, ast.Name) and t.id.isupper() for t in tugun.targets))
            if isinstance(tugun, (ast.Import, ast.ImportFrom, ast.FunctionDef)) or konst:
                qismlar.append(ast.unparse(tugun))
            else:
                tashlandi["skript"] += 1
    qolgan = [q for q in qismlar if not q.startswith(("import ", "from "))]
    ishlatilgan = {t.id for q in qolgan for t in ast.walk(ast.parse(q))
                   if isinstance(t, ast.Name)}
    importlar, keraksiz = set(), set()
    for q in qismlar:
        if q.startswith(("import ", "from ")):
            nom = q.split(" as ")[-1].split()[-1]
            (importlar if nom in ishlatilgan else keraksiz).add(q)
    importlar = sorted(importlar)
    tashlandi["keraksiz import"] = len(keraksiz)
    matn = '"""Notebookdan ajratilgan: tozalash va belgilar."""\n\n'
    if importlar:
        matn += "\n".join(importlar) + "\n\n\n"
    matn += "\n\n\n".join(qolgan) + "\n"
    return matn, tashlandi


def main() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        tmp = Path(tmp)
        nb1 = notebook_yarat(np.random.default_rng(0), qayta=0)
        nb2 = notebook_yarat(np.random.default_rng(1), qayta=1)   # qayta ishga tushirildi
        for nom, nb in [("xom_1.ipynb", nb1), ("xom_2.ipynb", nb2),
                        ("toza_1.ipynb", tozala_notebook(nb1)),
                        ("toza_2.ipynb", tozala_notebook(nb2))]:
            (tmp / nom).write_text(json.dumps(nb, indent=1, sort_keys=True),
                                   encoding="utf-8", newline="\n")

        print("=== 1. Chiqishlarni tozalash: hajm va sir ===")
        print(f"  {'fayl':<14} {'hajm KB':>8} {'kod katak':>10} {'chiqishlar':>11} "
              f"{'sir topildi':>12}")
        for nom in ["xom_1.ipynb", "toza_1.ipynb"]:
            matn = (tmp / nom).read_text(encoding="utf-8")
            nb = json.loads(matn)
            n_kod = sum(k["cell_type"] == "code" for k in nb["cells"])
            n_chiq = sum(len(k.get("outputs", [])) for k in nb["cells"])
            print(f"  {nom:<14} {len(matn.encode()) / 1024:>8.1f} {n_kod:>10} {n_chiq:>11} "
                  f"{len(SIR_REGEX.findall(matn)):>12}")

        print("\n=== 2. Git diff shovqini: bir xil kod, qayta ishga tushirilgan notebook ===")
        for a, b in [("xom_1.ipynb", "xom_2.ipynb"), ("toza_1.ipynb", "toza_2.ipynb")]:
            q1 = (tmp / a).read_text(encoding="utf-8").splitlines()
            q2 = (tmp / b).read_text(encoding="utf-8").splitlines()
            ozg = sum(1 for d in difflib.unified_diff(q1, q2, lineterm="", n=0)
                      if d[:1] in "+-" and not d.startswith(("+++", "---")))
            belgi = sum(len(d) for d in difflib.unified_diff(q1, q2, lineterm="", n=0)
                        if d[:1] in "+-" and not d.startswith(("+++", "---")))
            print(f"  {a} va {b}: o'zgargan qatorlar {ozg}, belgilar {belgi}")

        print("\n=== 3. Notebookdan modulga ===")
        matn, tashlandi = modulga(nb1)
        yol = tmp / "src" / "churn" / "tayyorlash.py"
        yol.parent.mkdir(parents=True)
        yol.write_text(matn, encoding="utf-8", newline="\n")
        print(f"  modul: src/churn/tayyorlash.py, {len(matn.splitlines())} qator")
        print(f"  tashlandi: magic {tashlandi['magic']} qator, "
              f"skript/ko'rsatish {tashlandi['skript']} ifoda, "
              f"ishlatilmagan import {tashlandi['keraksiz import']}")
        for q in matn.splitlines():
            if q.startswith(("import", "from", "def", "KECH")):
                print(f"    {q}")
        nomlar = {}
        exec(compile(matn, "tayyorlash.py", "exec"), nomlar)
        df = pd.DataFrame({"tolov": [100.0, -5.0, 100.0, 50.0], "oy": [12, 30, 12, 36]})
        natija = nomlar["belgi_qur"](nomlar["tozala"](df))
        print(f"  modul ishlaydi: {len(df)} qator -> {len(natija)} qator, "
              f"ustunlar {list(natija.columns)}")
        print(f"  manfiy to'lov qoldimi: {bool((natija['tolov'] < 0).any())}, "
              f"kechikkanlar: {int(natija['kechikkan'].sum())}")
        print(f"  modulda sir bormi: {bool(SIR_REGEX.search(matn))}; "
              f"'print' qoldimi: {'print(' in matn}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Chiqishlarni tozalash: hajm va sir ===
  fayl            hajm KB  kod katak  chiqishlar  sir topildi
  xom_1.ipynb       211.7          7           4            1
  toza_1.ipynb        1.7          7           0            0

=== 2. Git diff shovqini: bir xil kod, qayta ishga tushirilgan notebook ===
  xom_1.ipynb va xom_2.ipynb: o'zgargan qatorlar 20, belgilar 400524
  toza_1.ipynb va toza_2.ipynb: o'zgargan qatorlar 0, belgilar 0

=== 3. Notebookdan modulga ===
  modul: src/churn/tayyorlash.py, 16 qator
  tashlandi: magic 2 qator, skript/ko'rsatish 6 ifoda, ishlatilmagan import 3
    KECHIKISH_CHEGARASI = 30
    def tozala(df):
    def belgi_qur(df):
  modul ishlaydi: 4 qator -> 3 qator, ustunlar ['tolov', 'oy', 'yil', 'kechikkan']
  manfiy to'lov qoldimi: False, kechikkanlar: 1
  modulda sir bormi: False; 'print' qoldimi: False

Natija tahlili.

1-bo'lim — chiqishlarni tozalash. Xom notebook 211.7 KB: uning deyarli hammasi bitta rasm (base64 PNG) va HTML jadval. Tozalangan versiya — 1.7 KB, 100 barobardan ko'proq kichik; kod kataklari soni o'zgarmadi (7). Eng muhimi — xom notebookda bitta sir topildi: print('parol:', ...) katagining chiqishida parol ochiq turibdi. Kodda sir yo'q — u chiqishda; kodni ko'rib chiqqan odam uni sezmaydi. Tozalangan versiyada sir yo'q.

2-bo'lim — diff shovqini. Notebook kodi umuman o'zgarmadi, faqat qayta ishga tushirildi (ijro raqamlari va rasm boshqacha). Xom versiyalar orasida git 20 ta o'zgargan qator va 400 524 belgi farq ko'radi — code review da haqiqiy o'zgarishni topib bo'lmaydi. Tozalangan versiyalar orasida farq 0: kod o'zgarmagan bo'lsa, diff ham bo'sh. Aynan shuning uchun nbstripout git filtri yoki pre-commit sifatida ishlatiladi.

3-bo'lim — notebookdan modulga. ast bilan har katak tahlil qilindi: ikki magic qator (%matplotlib, !pip) va 6 ta skript/ko'rsatish ifodasi (df.head(), df2.plot(), print(...), ma'lumot yaratish) tashlandi; funksiyalar va katta harfli konstanta qoldi. Notebookdagi uchta import (numpy, pandas, os) funksiyalarda ishlatilmagani uchun modulga kirmadi. Natija — 16 qatorli src/churn/tayyorlash.py. Modul haqiqatan ishlaydi: 4 qatorli jadvaldan takror olib tashlandi (3 qator), manfiy to'lov qolmadi, 36 oylik mijoz "kechikkan" deb belgilandi. Modulda sir ham, print ham yo'q. Endi bu funksiyalarga test yozish mumkin — 3-misol.

Misol 3 — Testlar: oddiy holatlar va chegara holatlar, pytest va mini runner

python
"""Testlar: oddiy holatlar xatoni o'tkazib yuboradi, chegara holatlar ushlaydi."""

import importlib.util
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path

XATOLI = '''
def f1_ball(y_true, y_pred):
    tp = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 1)
    fp = sum(1 for t, p in zip(y_true, y_pred) if t == 0 and p == 1)
    fn = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 0)
    aniqlik = tp / (tp + fp)
    toliqlik = tp / (tp + fn)
    return 2 * aniqlik * toliqlik / (aniqlik + toliqlik)


def yosh_guruhi(yosh):
    if yosh <= 30:
        return "18-29"
    if yosh < 45:
        return "30-44"
    return "45+"


def telefon_tozala(s):
    raqam = "".join(ch for ch in s if ch.isdigit())
    return "+" + raqam


def vaqt_bolish(sanalar, ulush):
    tartib = sorted(range(len(sanalar)), key=lambda i: sanalar[i])
    k = int(len(sanalar) * (1 - ulush))
    return tartib[:k], tartib[k:]
'''

TUZATILGAN = '''
def f1_ball(y_true, y_pred):
    tp = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 1)
    fp = sum(1 for t, p in zip(y_true, y_pred) if t == 0 and p == 1)
    fn = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 0)
    if tp == 0:
        return 0.0
    aniqlik = tp / (tp + fp)
    toliqlik = tp / (tp + fn)
    return 2 * aniqlik * toliqlik / (aniqlik + toliqlik)


def yosh_guruhi(yosh):
    if yosh < 30:
        return "18-29"
    if yosh < 45:
        return "30-44"
    return "45+"


def telefon_tozala(s):
    raqam = "".join(ch for ch in s if ch.isdigit())
    if len(raqam) == 9:
        raqam = "998" + raqam
    if len(raqam) != 12 or not raqam.startswith("998"):
        raise ValueError("noto'g'ri raqam: " + s)
    return "+" + raqam


def vaqt_bolish(sanalar, ulush):
    tartib = sorted(range(len(sanalar)), key=lambda i: sanalar[i])
    k = int(len(sanalar) * (1 - ulush))
    return tartib[:k], tartib[k:]
'''

TEST_ODDIY = '''
from hisob import f1_ball, telefon_tozala, vaqt_bolish, yosh_guruhi


def test_f1_oddiy():
    assert abs(f1_ball([1, 0, 1, 1], [1, 0, 0, 1]) - 0.8) < 1e-9


def test_yosh_oddiy():
    assert [yosh_guruhi(y) for y in (25, 40, 60)] == ["18-29", "30-44", "45+"]


def test_telefon_oddiy():
    assert telefon_tozala("+998 90 123-45-67") == "+998901234567"


def test_vaqt_bolish_oddiy():
    sanalar = ["2026-03-01", "2026-01-01", "2026-02-01", "2026-04-01"]
    tr, te = vaqt_bolish(sanalar, 0.25)
    assert len(tr) == 3 and len(te) == 1
    assert max(sanalar[i] for i in tr) <= min(sanalar[i] for i in te)
'''

TEST_CHEGARA = '''
import random

from hisob import f1_ball, telefon_tozala, yosh_guruhi


def test_f1_ijobiy_bashorat_yoq():
    assert f1_ball([1, 1, 0], [0, 0, 0]) == 0.0


def test_f1_xossa_0_1_oraligida():
    r = random.Random(0)
    for _ in range(300):
        y = [r.randint(0, 1) for _ in range(3)]
        p = [r.randint(0, 1) for _ in range(3)]
        assert 0.0 <= f1_ball(y, p) <= 1.0


def test_yosh_chegaralar():
    assert [yosh_guruhi(y) for y in (29, 30, 44, 45)] == ["18-29", "30-44", "30-44", "45+"]


def test_telefon_kodsiz():
    assert telefon_tozala("90 123 45 67") == "+998901234567"


def test_telefon_notogri_rad_etiladi():
    try:
        telefon_tozala("12-34")
    except ValueError:
        return
    assert False, "ValueError kutilgan edi"
'''


def mini_runner(test_fayl, src):
    """pytest siz: test_ bilan boshlanuvchi funksiyalarni ishga tushiradi."""
    sys.modules.pop("hisob", None)
    sys.path.insert(0, str(src))
    try:
        spec = importlib.util.spec_from_file_location(test_fayl.stem, test_fayl)
        modul = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(modul)
        natija = {}
        for nom in sorted(n for n in dir(modul) if n.startswith("test_")):
            try:
                getattr(modul, nom)()
                natija[nom] = "o'tdi"
            except AssertionError:
                natija[nom] = "YIQILDI"
            except Exception as e:
                natija[nom] = f"XATO ({type(e).__name__})"
        return natija
    finally:
        sys.path.remove(str(src))
        sys.modules.pop("hisob", None)


def pytest_bilan(ildiz, src):
    """pytest bor bo'lsa - alohida jarayonda; faqat sonlar qaytariladi."""
    if importlib.util.find_spec("pytest") is None:
        return None
    env = dict(os.environ, PYTHONPATH=str(src), PYTHONDONTWRITEBYTECODE="1")
    r = subprocess.run([sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider",
                        "tests"], cwd=ildiz, env=env, capture_output=True, text=True,
                       encoding="utf-8", timeout=120)
    sonlar = dict((k, int(v)) for v, k in
                  re.findall(r"(\d+) (passed|failed|error)", r.stdout.splitlines()[-1]))
    return sonlar.get("passed", 0), sonlar.get("failed", 0) + sonlar.get("error", 0)


def main() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        ildiz = Path(tmp)
        src, tests = ildiz / "src", ildiz / "tests"
        src.mkdir()
        tests.mkdir()
        (tests / "test_oddiy.py").write_text(TEST_ODDIY, encoding="utf-8")
        (tests / "test_chegara.py").write_text(TEST_CHEGARA, encoding="utf-8")
        pytest_bor = importlib.util.find_spec("pytest") is not None
        print(f"pytest mavjud: {pytest_bor}")

        jami, yiqilgan_nomlar = {}, {}
        for versiya, kod in [("xatoli", XATOLI), ("tuzatilgan", TUZATILGAN)]:
            (src / "hisob.py").write_text(kod, encoding="utf-8")
            print(f"\n=== {versiya.upper()} hisob.py ===")
            jami[versiya], yiqilgan_nomlar[versiya] = {}, []
            for fayl in ["test_oddiy.py", "test_chegara.py"]:
                natija = mini_runner(tests / fayl, src)
                n_ok = sum(v == "o'tdi" for v in natija.values())
                jami[versiya][fayl] = (n_ok, len(natija) - n_ok)
                print(f"  {fayl:<16} o'tdi {n_ok}/{len(natija)}")
                for nom, holat in natija.items():
                    if holat != "o'tdi":
                        print(f"    {holat:<24} {nom}")
                        yiqilgan_nomlar[versiya].append(nom)
            o_tdi = sum(v[0] for v in jami[versiya].values())
            yiqildi = sum(v[1] for v in jami[versiya].values())
            pt = pytest_bilan(ildiz, src)
            if pt is None:
                print("  pytest yo'q - faqat mini runner natijasi")
            else:
                print(f"  pytest: o'tdi {pt[0]}, yiqildi {pt[1]}; mini runner bilan mos: "
                      f"{pt == (o_tdi, yiqildi)}")

        print("\n=== Xulosa ===")
        x = jami["xatoli"]
        funksiyalar = sorted({n.split("_")[1] for n in yiqilgan_nomlar["xatoli"]})
        print(f"  xatoli versiya: oddiy testlarda yiqilgan {x['test_oddiy.py'][1]}, "
              f"chegara testlarida {x['test_chegara.py'][1]}")
        print(f"  xato topilgan funksiyalar: {', '.join(funksiyalar)}")
        print(f"  tuzatilgan versiya: yiqilgan "
              f"{sum(v[1] for v in jami['tuzatilgan'].values())}")
        if x["test_oddiy.py"][1] == 0 and x["test_chegara.py"][1] > 0:
            print("  faqat oddiy testlar bilan CI 'yashil' bo'lardi - xatolar ishlab "
                  "chiqarishga o'tardi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
pytest mavjud: True

=== XATOLI hisob.py ===
  test_oddiy.py    o'tdi 4/4
  test_chegara.py  o'tdi 0/5
    XATO (ZeroDivisionError) test_f1_ijobiy_bashorat_yoq
    XATO (ZeroDivisionError) test_f1_xossa_0_1_oraligida
    YIQILDI                  test_telefon_kodsiz
    YIQILDI                  test_telefon_notogri_rad_etiladi
    YIQILDI                  test_yosh_chegaralar
  pytest: o'tdi 4, yiqildi 5; mini runner bilan mos: True

=== TUZATILGAN hisob.py ===
  test_oddiy.py    o'tdi 4/4
  test_chegara.py  o'tdi 5/5
  pytest: o'tdi 9, yiqildi 0; mini runner bilan mos: True

=== Xulosa ===
  xatoli versiya: oddiy testlarda yiqilgan 0, chegara testlarida 5
  xato topilgan funksiyalar: f1, telefon, yosh
  tuzatilgan versiya: yiqilgan 0
  faqat oddiy testlar bilan CI 'yashil' bo'lardi - xatolar ishlab chiqarishga o'tardi

Natija tahlili.

Xatoli versiyada oddiy testlar 4/4 o'tdi. Agar loyihada faqat shu testlar bo'lganida, CI "yashil" bo'lardi va uchta funksiyadagi xato ishlab chiqarishga o'tardi. Chegara testlari 5 tadan 5 tasida yiqildi:

  • f1_ball — musbat bashorat bo'lmaganda ZeroDivisionError. Buni ikki test ushladi: aniq chegara holat va xossa testi ("F1 har doim [0, 1] da", 300 ta tasodifiy kirish). Xossa testi xatoning aniq joyini bilmasdan ham uni topdi — tasodifiy 3 elementli bashoratlarning taxminan sakkizdan biri hammasi nol.
  • yosh_guruhi — <= va < farqi: 30 yoshli mijoz "18-29" guruhiga tushgan. Oddiy test (25, 40, 60) chegaraga yaqinlashmaydi.
  • telefon_tozala — mamlakat kodisiz raqamni noto'g'ri qaytaradi va noto'g'ri formatni rad etmaydi.

Tuzatilgan versiyada 9/9 o'tdi. Testlar ikki usulda ishga tushirildi: o'zimiz yozgan mini runner (jarayon ichida, test_ funksiyalarni topib chaqiradi) va, pytest mavjud bo'lgani uchun, alohida jarayonda python -m pytest. Ikkalasining sonlari mos keldi — mini runner pytest ning asosiy g'oyasini to'g'ri takrorlaydi: test — bu assert li oddiy funksiya. Pytest bundan tashqari parametrlash, fixture lar, chiroyli xato xabarlari va plaginlar beradi.

Misol 4 — Git vaqtinchalik papkada: nima kuzatiladi, blob xeshi va ma'lumot ko'rsatkichi

python
"""Git DS loyihasida: nima kuzatiladi, blob xeshi va ma'lumot uchun "ko'rsatkich" fayl."""

import hashlib
import json
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path

COMMIT_QOIDA = re.compile(r"^(feat|fix|data|docs|test|refactor|chore)(\([\w-]+\))?: \S.{8,70}$")


def yoz(ildiz, nisbiy, matn):
    p = ildiz / nisbiy
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_bytes(matn.encode("utf-8"))


def blob_xesh(baytlar):
    """Git blob xeshi: sha1(b"blob <uzunlik>\\0" + tarkib) - vaqtga bog'liq emas."""
    return hashlib.sha1(b"blob %d\x00" % len(baytlar) + baytlar).hexdigest()


def git(ildiz, *args, env=None):
    r = subprocess.run(["git", "-c", "core.autocrlf=false", "-c", "init.defaultBranch=main",
                        "-c", "user.name=Namuna", "-c", "user.email=namuna@example.com",
                        "-c", "commit.gpgsign=false", *args],
                       cwd=ildiz, env=env, capture_output=True, text=True,
                       encoding="utf-8", timeout=60)
    return r.stdout.strip()


def korsatkich(ildiz, nisbiy):
    """DVC g'oyasi: katta fayl o'rniga git ga uning xeshi va hajmi yoziladi."""
    b = (ildiz / nisbiy).read_bytes()
    return json.dumps({"yol": nisbiy, "md5": hashlib.md5(b).hexdigest(),
                       "hajm": len(b)}, indent=1, sort_keys=True) + "\n"


def main() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        ildiz = Path(tmp) / "churn"
        yoz(ildiz, "README.md", "# Mijoz ketishi\n\nTest AUC 0.81.\n")
        yoz(ildiz, ".gitignore", "data/raw/\ndata/processed/\n.env\nmodels/\n__pycache__/\n")
        yoz(ildiz, "src/churn/__init__.py", "")
        yoz(ildiz, "src/churn/belgilar.py", "def yil(oy):\n    return oy / 12\n")
        yoz(ildiz, "tests/test_belgilar.py",
            "from churn.belgilar import yil\n\n\ndef test_yil():\n    assert yil(24) == 2\n")
        yoz(ildiz, "configs/asos.yaml", "model: histgb\nseed: 0\n")
        qatorlar = "".join(f"{i},{18 + i % 50},{i % 7}\n" for i in range(50_000))
        yoz(ildiz, "data/raw/mijozlar.csv", "id,yosh,shikoyat\n" + qatorlar)
        yoz(ildiz, ".env", "DB_PAROL=namuna_parol_git_ga_kirmasin\n")
        yoz(ildiz, "models/model.pkl", "x" * 200_000)
        yoz(ildiz, "data/mijozlar.csv.json", korsatkich(ildiz, "data/raw/mijozlar.csv"))

        print("=== 1. Tarkib xeshi: git ga bog'liq emas ===")
        b = (ildiz / "README.md").read_bytes()
        print(f"  README.md blob xeshi (o'zimiz): {blob_xesh(b)[:12]}")
        yoz(ildiz, "nusxa/README_nusxa.md", b.decode())
        print(f"  bir xil tarkibli boshqa fayl:  "
              f"{blob_xesh((ildiz / 'nusxa/README_nusxa.md').read_bytes())[:12]}")
        shutil.rmtree(ildiz / "nusxa")
        print(f"  bitta belgi o'zgarsa:          {blob_xesh(b.replace(b'0.81', b'0.82'))[:12]}")

        if shutil.which("git") is None:
            print("\ngit topilmadi - qolgan qism faqat xesh g'oyasi bilan cheklanadi")
            return
        env = dict(os.environ, GIT_CONFIG_NOSYSTEM="1",
                   GIT_CONFIG_GLOBAL=str(Path(tmp) / "bosh.gitconfig"))
        (Path(tmp) / "bosh.gitconfig").write_text("", encoding="utf-8")

        print("\n=== 2. git init, .gitignore va nima kuzatiladi ===")
        git(ildiz, "init", "-q", env=env)
        print(f"  git bilan bir xil xesh: "
              f"{git(ildiz, 'hash-object', 'README.md', env=env) == blob_xesh(b)}")
        git(ildiz, "add", "-A", env=env)
        kuzatiladi = git(ildiz, "ls-files", env=env).splitlines()
        barcha = sorted(p.relative_to(ildiz).as_posix() for p in ildiz.rglob("*")
                        if p.is_file() and ".git" not in p.relative_to(ildiz).parts)
        print(f"  papkadagi fayllar {len(barcha)}, git kuzatadi {len(kuzatiladi)}")
        for f in kuzatiladi:
            print(f"    + {f}")
        for f in sorted(set(barcha) - set(kuzatiladi)):
            qoida = git(ildiz, "check-ignore", "-v", f, env=env).split("\t")[0]
            hajm = (ildiz / f).stat().st_size / 1024
            print(f"    - {f:<24} {hajm:>7.1f} KB  qoida {qoida}")
        git(ildiz, "commit", "-q", "-m", "feat: loyiha tuzilishi va birinchi belgi", env=env)

        print("\n=== 3. Ma'lumot o'zgardi: git nimani ko'radi? ===")
        with open(ildiz / "data/raw/mijozlar.csv", "a", encoding="utf-8") as f:
            f.write("50000,33,1\n")
        print(f"  xom fayl o'zgargach git status: "
              f"{git(ildiz, 'status', '--porcelain', env=env) or '(toza - data/raw kuzatilmaydi)'}")
        yoz(ildiz, "data/mijozlar.csv.json", korsatkich(ildiz, "data/raw/mijozlar.csv"))
        print(f"  ko'rsatkich yangilangach: {git(ildiz, 'status', '--porcelain', env=env)}")
        print("  " + git(ildiz, "diff", "--stat", env=env).splitlines()[-1].strip())
        git(ildiz, "commit", "-qam", "data: mijozlar ro'yxati yangilandi (+1 qator)", env=env)

        print("\n=== 4. Commit xabarlari sifati ===")
        (ildiz / "src/churn/belgilar.py").write_text(
            "def yil(oy):\n    return round(oy / 12, 2)\n", encoding="utf-8")
        git(ildiz, "commit", "-qam", "update", env=env)
        tarix = git(ildiz, "log", "--format=%s", env=env).splitlines()
        print(f"  commitlar soni: {len(tarix)}")
        namunalar = tarix[::-1] + ["fix(belgilar): yil 0 oyda ham to'g'ri ishlaydi",
                                   "asdf", "final final v2 REAL", "wip"]
        yaxshi = 0
        for x in namunalar:
            ok = bool(COMMIT_QOIDA.match(x))
            yaxshi += ok
            print(f"    {'OK ' if ok else 'YOQ'} {x}")
        print(f"  qoidaga mos: {yaxshi}/{len(namunalar)}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Tarkib xeshi: git ga bog'liq emas ===
  README.md blob xeshi (o'zimiz): 757fbe210b62
  bir xil tarkibli boshqa fayl:  757fbe210b62
  bitta belgi o'zgarsa:          c37c0ee5471f

=== 2. git init, .gitignore va nima kuzatiladi ===
  git bilan bir xil xesh: True
  papkadagi fayllar 10, git kuzatadi 7
    + .gitignore
    + README.md
    + configs/asos.yaml
    + data/mijozlar.csv.json
    + src/churn/__init__.py
    + src/churn/belgilar.py
    + tests/test_belgilar.py
    - .env                         0.0 KB  qoida .gitignore:3:.env
    - data/raw/mijozlar.csv      526.3 KB  qoida .gitignore:1:data/raw/
    - models/model.pkl           195.3 KB  qoida .gitignore:4:models/

=== 3. Ma'lumot o'zgardi: git nimani ko'radi? ===
  xom fayl o'zgargach git status: (toza - data/raw kuzatilmaydi)
  ko'rsatkich yangilangach: M data/mijozlar.csv.json
  1 file changed, 2 insertions(+), 2 deletions(-)

=== 4. Commit xabarlari sifati ===
  commitlar soni: 3
    OK  feat: loyiha tuzilishi va birinchi belgi
    OK  data: mijozlar ro'yxati yangilandi (+1 qator)
    YOQ update
    OK  fix(belgilar): yil 0 oyda ham to'g'ri ishlaydi
    YOQ asdf
    YOQ final final v2 REAL
    YOQ wip
  qoidaga mos: 3/7

Natija tahlili.

1-bo'lim — git blob xeshi sha1("blob <uzunlik>\0" + tarkib) — faqat tarkibga bog'liq: README va uning aynan nusxasi bir xil xesh (757fbe210b62) oladi, bitta raqam (0.81 -> 0.82) o'zgarsa — butunlay boshqa xesh. Shuning uchun blob xeshlari deterministik, commit xeshlari esa emas (ularga muallif va vaqt ham kiradi — shu sababli bu misolda commit xeshlari chop etilmaydi).

2-bo'lim — git hash-object bizning funksiyamiz bilan bir xil natija berdi (True). Papkada 10 fayl, git 7 tasini kuzatadi: kod, test, konfig, README, .gitignore va ma'lumot ko'rsatkichi. git check-ignore -v har e'tiborsiz faylni qaysi qoida chiqarib tashlaganini ko'rsatadi: .env — 3-qator, 526 KB li xom ma'lumot — 1-qator (data/raw/), 195 KB li model — 4-qator.

3-bo'lim — ma'lumot o'zgardi (bitta qator qo'shildi). Git buni ko'rmaydi: data/raw kuzatilmaydi, status toza. Ma'lumot versiyasini kuzatish uchun ko'rsatkich faylini yangiladik — endi git bitta kichik JSON fayl o'zgarganini ko'radi: 2 qator qo'shildi, 2 qator o'chdi (md5 va hajm). Bu — 27.5 dagi DVC g'oyasining o'zi: git ma'lumotning qaysi versiyasi ishlatilganini biladi, ma'lumotning o'zini esa saqlamaydi. Kod commiti va ko'rsatkich birga — "bu natija qaysi kod va qaysi ma'lumotdan?" degan savolga aniq javob.

4-bo'lim — commit xabarlari. Repo da 3 ta commit: ikkitasi qoidaga mos (feat: ..., data: ...), uchinchisi — update. Namunalar bilan birga 3/7 mos: asdf, final final v2 REAL, wip kabi xabarlar tarixni o'qib bo'lmaydigan qiladi. Bunday linter commit-msg hook sifatida sozlanadi va yomon xabarni commit bosqichidayoq rad etadi.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"Repo — kodni saqlash joyi, tuzilish muhim emas" Repo — loyihaning yuzi; tuzilish va README birinchi daqiqada baholanadi
"Ma'lumotni ham git ga qo'yaman — qulay" Tarix abadiy shishadi; ma'lumot — ko'rsatkich fayl yoki tashqi omborda
"Kalitni keyingi commitda o'chirdim — xavfsiz" Tarixda qoladi; avval kalitni bekor qilish (rotate)
"Sir skaneri hamma sirni topadi" Qisqa parollarni o'tkazib yuboradi (1-misol: 7/8); asosiy himoya — sirni kodga yozmaslik
"Notebook chiqishlari — foydali hujjat" Hajm, diff shovqini va sir sizishi (2-misol: 211.7 KB va 1.7 KB)
"Testlar o'tdi — kod to'g'ri" Oddiy holat testlari 4/4 o'tdi, xatolar esa chegarada edi (3-misol)
"README — oxirida yoziladigan rasmiyat" README — birinchi o'qiladigan fayl; natija tepada bo'lishi kerak
"Commit xabari muhim emas" Tarix — ish uslubining yozuvi; "update" hech narsa demaydi

6. Keng tarqalgan xatolar va yechimlari

1. Ma'lumot git da

python
# git add data/mijozlar.csv                                         # ⚠️
# .gitignore: data/raw/  +  data/mijozlar.csv.json (xesh, hajm)      # ✅

2. Kalit kodda

python
API_KEY = "a8Kq...Zt"                                                # ⚠️
API_KEY = os.environ["API_KEY"]         # .env git da emas           # ✅

3. .gitignore kech yozildi

python
# .gitignore ga data/ qo'shildi, lekin fayl allaqachon kuzatilmoqda  # ⚠️
# git rm -r --cached data/  ->  commit  (va tarixni tekshirish)       # ✅

4. Notebook chiqishlari bilan

python
# git add 01-eda.ipynb   (211.7 KB, ichida parol chop etilgan)         # ⚠️
# pre-commit: nbstripout -> 1.7 KB, sir yo'q                          # ✅

5. Faqat oddiy testlar

python
def test_yosh(): assert yosh_guruhi(25) == "18-29"                   # ⚠️
@pytest.mark.parametrize("y, k", [(29, "18-29"), (30, "30-44"), (45, "45+")])  # ✅
def test_yosh_chegara(y, k): assert yosh_guruhi(y) == k

6. Qadalmagan qaramliklar

python
# requirements.txt:  pandas \n scikit-learn                          # ⚠️
# pyproject.toml:    "pandas==3.0.1", "scikit-learn==1.9.0"           # ✅

7. Ma'nosiz commit xabari

python
# git commit -m "update"                                             # ⚠️
# git commit -m "fix(tozalash): manfiy to'lovlar 0 ga kesiladi"      # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 21.11-dars (o'tilgan): Loyiha tuzilishi va takrorlanuvchanlik — modullar, konfig, kirish nuqtasi
  • 27.2-dars (o'tilgan): Reproduksiya va muhit — qadalgan versiyalar, manifest
  • 27.5-dars (o'tilgan): Model versiyalash va registr — DVC g'oyasi, kontent-manzilli saqlash
  • 27.8-dars (o'tilgan): API ni testlash — kontrakt, golden va chegara testlari
  • 27.14-dars (o'tilgan): Amaliyot — Dockerfile va CI/CT konfiguratsiyasi
  • 29.5-dars (o'tilgan): Kaggle — musobaqa kodini toza repo ga aylantirish
  • 29.7-dars: Portfolio — har loyiha shu tuzilishdagi repo; README sifati o'lchanadi
  • 29.8-dars: Rezyume — GitHub havolasi rezyumening bir qismi

8. Eng yaxshi amaliyotlar

  1. Birinchi commit: tuzilish, .gitignore, .env.example, README skeleti.

  2. Ma'lumot va modellar git da emas; ularning versiyasi — xesh bilan.

  3. Sirlar faqat muhit o'zgaruvchisida; commit qilingan sir — darhol rotate.

  4. README ning birinchi ekranida — nima, kim uchun, asosiy natija raqami.

  5. Notebook chiqishlarsiz; qayta ishlatiladigan kod src/ da.

  6. Har funksiya bilan chegara holat testlari; CI da lint va pytest.

  7. Kichik, mantiqiy commitlar va ma'noli xabarlar.

  8. Repo ni ochiq qilishdan oldin — avtomatik audit (1-misol).


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # yomon repo (README bitta qator, .gitignore yo'q, kalit kodda): auditor bali?
2.  # uchta tez tuzatish (.gitignore, kalit -> os.environ, versiyalar): ball?
3.  # 'parol = "Qwerty123"' - entropiya+regex detektori topadimi?
4.  # notebook chiqishlari tozalangach hajm necha barobar kamayadi?
5.  # kodi o'zgarmagan notebook qayta ishga tushirildi: xom va toza versiyada diff?
6.  # oddiy testlar 4/4 o'tdi - xatoli funksiyalar bormi?
7.  # data/raw dagi fayl o'zgarsa, git status nima ko'rsatadi?
8.  # README.md va uning aynan nusxasi - git blob xeshlari?
9.  # "update" commit xabari qoidaga mosmi?
Javoblar
  1. 3/100 (1-misol)
  2. 60/100; README, testlar, notebooklar va CI qoladi
  3. Yo'q — 9 belgi, regex 12 talab qiladi (detektor 7/8)
  4. 100 barobardan ko'p (211.7 KB va 1.7 KB)
  5. Xom: 20 qator, 400 524 belgi; toza: 0
  6. Ha — uchta funksiyada xato, chegara testlari 5/5 yiqildi
  7. Hech narsa — data/raw/ e'tiborsiz; ko'rsatkich yangilanganda o'sha fayl o'zgaradi
  8. Bir xil (757fbe210b62) — xesh faqat tarkibga bog'liq
  9. Yo'q — <tur>: <tavsif> ko'rinishida emas

Vazifa 2: Xatolarni tuzating

python
1.  DB_URL = "postgresql://admin:parol123@10.0.0.5/prod"

2.  # .gitignore
    data

3.  def test_f1():
        assert f1_ball([1, 0, 1], [1, 0, 1]) == 1.0

4.  # notebooks/Untitled3.ipynb - 4 MB, chiqishlar bilan

5.  # README.md
    # project
    run main.py
Javoblar
python
1.  DB_URL = os.environ["DB_URL"]          # .env.example da faqat DB_URL=

2.  # .gitignore
    data/raw/
    data/processed/
    models/
    .env

3.  def test_f1_ijobiy_bashorat_yoq():
        assert f1_ball([1, 1, 0], [0, 0, 0]) == 0.0
    # + xossa testi: 0 <= f1 <= 1 tasodifiy kirishlarda

4.  # notebooks/01-eda.ipynb - nbstripout bilan tozalangan; kod src/ da

5.  # README: bir gaplik xulosa + natija, Ma'lumot, O'rnatish,
    # Foydalanish (aniq buyruq), Cheklovlar

Vazifa 3: Repo auditori

Modellang (1-misol asosida):

  1. Auditorga LICENSE fayl mavjudligi va README da "Litsenziya" bo'limi tekshiruvini qo'shing (vazn 5)
  2. .gitignore ning ! (istisno) va ** qoidalarini qo'llab-quvvatlang — git check-ignore bilan solishtiring
  3. Sir detektorini o'z repolaringizda ishga tushiring: nechta noto'g'ri signal (false positive) chiqdi?
  4. Auditor natijasini JSON ga yozing va CI da "ball < 70 bo'lsa yiqil" qoidasini qo'shing

Vazifa 4: Notebook

Modellang (2-misol asosida):

  1. modulga funksiyasiga class lar va @dataclass larni qo'shing
  2. Notebookdagi df.head() chiqishida shaxsiy ma'lumot (telefon raqami) bor — regex bilan toping
  3. Jupytext g'oyasi: har kod katagini # %% ajratgichi bilan .py faylga yozing va qaytadan notebookka yig'ing — aylanma o'tish yo'qotishsizmi?

Vazifa 5: Testlar

Modellang (3-misol asosida):

  1. vaqt_bolish uchun chegara testlari: bo'sh ro'yxat, ulush=0, bir xil sanalar
  2. Mini runnerga @parametrize g'oyasini qo'shing (ro'yxat bo'yicha takrorlash)
  3. Xossa testini "shrinking" bilan to'ldiring: xato topilganda eng qisqa kirishni qidiring

Vazifa 6: Git

Modellang (4-misol asosida):

  1. Tasodifan commit qilingan .env ni git rm --cached bilan chiqaring — git log da u hali ham ko'rinadimi?
  2. O'z "kontent-manzilli omboringiz": kesh/ab/cdef... papkasiga fayllarni xesh bo'yicha saqlang va ko'rsatkichdan tiklang
  3. Commit xabarlari linterini commit-msg hook sifatida sozlang

Vazifa 7: O'ylash

Do'stingiz: "Portfolio uchun loyihamni GitHub ga joylayapman. Ma'lumotni ham qo'shaman — 300 MB, boshqalar yuklab olishi qulay bo'lsin. Notebookni chiqishlari bilan qoldiraman — rasmlar ko'rinib tursin, README yozishga vaqt yo'q. Kecha tasodifan config.py da bank API kalitini commit qilib qo'ygan edim, bugun o'chirdim, endi hammasi joyida."

Javob

Qisqa javob: uchta rejadan ikkitasi repo ni yomonlashtiradi, uchinchisi esa xavfli — kalit hali ham ochiq.

1. Eng shoshilinch: bank API kaliti. Kalitni keyingi commitda o'chirish uni tarixdan olib tashlamaydi: git log -p bilan har kim ko'radi, ochiq repolarni avtomatik skanerlaydigan botlar esa buni tez topishi mumkin. To'g'ri tartib:

python
# 1) HOZIROQ: bank tizimida kalitni bekor qilish va yangisini yaratish (rotate)
# 2) yangi kalit - faqat .env da (.gitignore da), kodda os.environ["BANK_API_KEY"]
# 3) tarixni tozalash (git filter-repo kabi vosita) - lekin bu 1-qadamning
#    o'rnini bosmaydi: kalit allaqachon nusxalangan bo'lishi mumkin

2. 300 MB ma'lumot. Git uni tarixga abadiy yozadi va GitHub katta fayllarni qabul qilmaydi. Yaxshisi: ma'lumot manbasiga havola va uni yuklab/yaratib oladigan skript, yoki ko'rsatkich fayl (4-misol), kerak bo'lsa — alohida ma'lumot xostingi. Ma'lumotni qayta tarqatish huquqi borligini ham tekshiring 2.9-bob.

3. Chiqishli notebook, README siz. Rasmlar ko'rinib turishi yaxshi niyat, lekin 2-misolda chiqishlar hajmni 100 barobardan ko'proq oshirdi va parolni ochiq qoldirdi. Rasmlarni reports/figures/ ga saqlab, README da ko'rsatish yaxshiroq. README siz repo ni ish beruvchi 30 soniyada yopadi 29.7-bob — bir gaplik xulosa va natija jadvali 20 daqiqa oladi.

Do'stga javob: "Birinchi navbatda bank kalitini bugun bekor qil — o'chirish yetarli emas, u tarixda turibdi. Ma'lumotni repo ga qo'yma, manbaga havola va skript yetarli. Notebookni tozalab, asosiy rasmlarni README ga qo'y — bu 20 daqiqalik ish, lekin repo ni ochgan odam uchun farqi katta."

Nimani mustahkamlaydi: 2.1, 2.2, 2.4, 2.5, 2.6-bo'limlar.


Xulosa

Bu darsda ma'lumotlar fani loyihasining repo sini ko'rsatiladigan va xavfsiz holga keltirishni o'rgandik.

Eng muhim uch fikr:

  1. Repo — loyihaning yuzi va uni avtomatik tekshirish mumkin. 1-misolda yomon repo 3/100, yaxshisi 100/100 oldi; uchta tez tuzatish 3 -> 60 berdi. Sir detektori foydali, lekin mukammal emas (7/8 — qisqa parolni o'tkazib yubordi): asosiy himoya — sirni kodga yozmaslik, commit qilingan sirni esa darhol bekor qilish.

  2. Git ga kod va ko'rsatkichlar kiradi — ma'lumot, modellar, sirlar va notebook chiqishlari emas. 2-misolda chiqishlarni tozalash notebookni 211.7 KB dan 1.7 KB ga kichraytirdi, chiqishdagi parolni olib tashladi va qayta ishga tushirishdagi diff shovqinini 20 qatordan 0 ga tushirdi. 4-misolda git 10 fayldan 7 tasini kuzatdi, xom ma'lumot o'zgarishi esa faqat kichik ko'rsatkich fayl orqali ko'rindi (DVC g'oyasi).

  3. Xatolar chegarada yashaydi. 3-misolda oddiy testlar xatoli kodda 4/4 o'tdi, chegara va xossa testlari esa uchta funksiyadagi xatoni ushladi (5/5 yiqildi); pytest va o'zimizning mini runner bir xil natija berdi. CI da lint va shunday testlar — "yashil" belgisi haqiqatan nimanidir anglatishi uchun.

Keyingi darsda Portfolio yaratish: ish beruvchi portfolio ga bir-ikki daqiqada nimani qaraydi, yaxshi portfolio loyihasining mezonlari, kurs amaliyot darslarini xilma-xil loyihalarga aylantirish, README va natija raqamlarining mosligini avtomatik tekshirish, ko'nikmalar matritsasi va "30 soniyalik ko'rib chiqish" simulyatsiyasi.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
29.6-dars: GitHub va loyiha tuzilishi — IlmHamroh