Mundarija (24)
- 1. Kirish va motivatsiya
- 2. Nazariya — chuqur tushuntirish
- 2.1. DS loyiha repo tuzilishi
- 2.2. Git ma'lumotlar fani uchun: nima kiradi, nima kirmaydi
- 2.3. Yaxshi commit tarixi
- 2.4. README anatomiyasi
- 2.5. Notebookdan modulga
- 2.6. Testlar (pytest uslubi)
- 2.7. CI: GitHub Actions
- 2.8. pyproject.toml, lint va pre-commit
- 2.9. Litsenziya va ma'lumot huquqlari
- 2.10. Tuzoqlar
- 3. Tez ma'lumotnoma
- 4. Batafsil misollar
- Misol 1 — Repo auditori: yomon va yaxshi repo, sirlar va ball
- Misol 2 — Notebookdan modulga: chiqishlarni tozalash, diff shovqini va sir sizishi
- Misol 3 — Testlar: oddiy holatlar va chegara holatlar, pytest va mini runner
- Misol 4 — Git vaqtinchalik papkada: nima kuzatiladi, blob xeshi va ma'lumot ko'rsatkichi
- 5. To'g'ri va noto'g'ri tushunishlar
- 6. Keng tarqalgan xatolar va yechimlari
- 7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 8. Eng yaxshi amaliyotlar
- 9. Amaliy topshiriq
- Xulosa
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.
gitvapytestmavjud 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.
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
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 qachonNega 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.
.gitignore - DS loyihasi uchun namuna
data/raw/
data/interim/
data/processed/
models/
.env
__pycache__/
*.pyc
.ipynb_checkpoints/
.venv/
mlruns/
*.logSirlar. 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
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.
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# 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:
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()) - tashlanadi2.6. Testlar (pytest uslubi)
27.8 da API testlarini o'rgandik. DS loyihasida testlar kodning mantiqini himoya qiladi — model sifatini emas (u baholash skriptida):
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 -qimport 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:
# .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 testCI 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 signali2.8. pyproject.toml, lint va pre-commit
[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.)
# .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 tozalashLint — 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
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)}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 test4. Batafsil misollar
Misollar real Python bilan (Python 3.14). Har misol mustaqil ishlaydi va hamma fayllarni vaqtinchalik papkada yaratadi;
gitvapytestmavjudligi tekshiriladi.
Misol 1 — Repo auditori: yomon va yaxshi repo, sirlar va ball
"""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:
=== 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/8Natija 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
"""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:
=== 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: FalseNatija 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
"""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:
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'tardiNatija 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'lmagandaZeroDivisionError. 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
"""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:
=== 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/7Natija 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
# git add data/mijozlar.csv # ⚠️
# .gitignore: data/raw/ + data/mijozlar.csv.json (xesh, hajm) # ✅2. Kalit kodda
API_KEY = "a8Kq...Zt" # ⚠️
API_KEY = os.environ["API_KEY"] # .env git da emas # ✅3. .gitignore kech yozildi
# .gitignore ga data/ qo'shildi, lekin fayl allaqachon kuzatilmoqda # ⚠️
# git rm -r --cached data/ -> commit (va tarixni tekshirish) # ✅4. Notebook chiqishlari bilan
# 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
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) == k6. Qadalmagan qaramliklar
# requirements.txt: pandas \n scikit-learn # ⚠️
# pyproject.toml: "pandas==3.0.1", "scikit-learn==1.9.0" # ✅7. Ma'nosiz commit xabari
# 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
Birinchi commit: tuzilish,
.gitignore,.env.example, README skeleti.Ma'lumot va modellar git da emas; ularning versiyasi — xesh bilan.
Sirlar faqat muhit o'zgaruvchisida; commit qilingan sir — darhol rotate.
README ning birinchi ekranida — nima, kim uchun, asosiy natija raqami.
Notebook chiqishlarsiz; qayta ishlatiladigan kod
src/da.Har funksiya bilan chegara holat testlari; CI da lint va pytest.
Kichik, mantiqiy commitlar va ma'noli xabarlar.
Repo ni ochiq qilishdan oldin — avtomatik audit (1-misol).
9. Amaliy topshiriq
Vazifa 1: Bashorat qiling
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
3/100(1-misol)60/100; README, testlar, notebooklar va CI qoladi- Yo'q — 9 belgi, regex 12 talab qiladi (detektor 7/8)
- 100 barobardan ko'p (
211.7KB va1.7KB) - Xom: 20 qator, 400 524 belgi; toza: 0
- Ha — uchta funksiyada xato, chegara testlari 5/5 yiqildi
- Hech narsa —
data/raw/e'tiborsiz; ko'rsatkich yangilanganda o'sha fayl o'zgaradi - Bir xil (
757fbe210b62) — xesh faqat tarkibga bog'liq - Yo'q —
<tur>: <tavsif>ko'rinishida emas
Vazifa 2: Xatolarni tuzating
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.pyJavoblar
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), CheklovlarVazifa 3: Repo auditori
Modellang (1-misol asosida):
- Auditorga
LICENSEfayl mavjudligi va README da "Litsenziya" bo'limi tekshiruvini qo'shing (vazn 5) .gitignorening!(istisno) va**qoidalarini qo'llab-quvvatlang —git check-ignorebilan solishtiring- Sir detektorini o'z repolaringizda ishga tushiring: nechta noto'g'ri signal (false positive) chiqdi?
- Auditor natijasini JSON ga yozing va CI da "ball < 70 bo'lsa yiqil" qoidasini qo'shing
Vazifa 4: Notebook
Modellang (2-misol asosida):
modulgafunksiyasiga class lar va@dataclasslarni qo'shing- Notebookdagi
df.head()chiqishida shaxsiy ma'lumot (telefon raqami) bor — regex bilan toping - Jupytext g'oyasi: har kod katagini
# %%ajratgichi bilan.pyfaylga yozing va qaytadan notebookka yig'ing — aylanma o'tish yo'qotishsizmi?
Vazifa 5: Testlar
Modellang (3-misol asosida):
vaqt_bolishuchun chegara testlari: bo'sh ro'yxat,ulush=0, bir xil sanalar- Mini runnerga
@parametrizeg'oyasini qo'shing (ro'yxat bo'yicha takrorlash) - Xossa testini "shrinking" bilan to'ldiring: xato topilganda eng qisqa kirishni qidiring
Vazifa 6: Git
Modellang (4-misol asosida):
- Tasodifan commit qilingan
.envnigit rm --cachedbilan chiqaring —git logda u hali ham ko'rinadimi? - O'z "kontent-manzilli omboringiz":
kesh/ab/cdef...papkasiga fayllarni xesh bo'yicha saqlang va ko'rsatkichdan tiklang - Commit xabarlari linterini
commit-msghook 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:
# 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 mumkin2. 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:
Repo — loyihaning yuzi va uni avtomatik tekshirish mumkin. 1-misolda yomon repo
3/100, yaxshisi100/100oldi; uchta tez tuzatish3 -> 60berdi. 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.Git ga kod va ko'rsatkichlar kiradi — ma'lumot, modellar, sirlar va notebook chiqishlari emas. 2-misolda chiqishlarni tozalash notebookni
211.7KB dan1.7KB ga kichraytirdi, chiqishdagi parolni olib tashladi va qayta ishga tushirishdagi diff shovqinini20qatordan0ga 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).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.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!