DuckDB engine va "bir xil natija"
Plandan SQL gacha
8-bobda planni bajaradigan run_plan ni ko'rdik: har bir qadam yangi jadval yaratadi. Bu bob
bir qadam ichiga kiradi: tool plandagi argumentlardan qanday SQL yasaydi, bank fayllaridagi
iflos qiymatlar bilan nima qiladi, va testlarda "model to'g'ri javob berdi" degan hukm qanday
chiqariladi.
Hamma toollar bitta faylda — accuracy/tools.py. Har bir tool — oddiy Python funksiyasi: u
manba jadval nomini, yangi jadval nomini va argumentlarni oladi, SQL yasaydi va DuckDB ga beradi.
Qiymat — parametr, nom — qo'shtirnoq ichida
Model yozgan qiymat SQL matniga hech qachon to'g'ridan-to'g'ri qo'shilmaydi. filter toolining
shart yasaydigan qismi:
import inspect from accuracy import tools print(inspect.getsource(tools._condition))
def _condition(args):
column = q(args["column"])
if args["op"] == "is_empty":
return _empty(column), []
if args["op"] == "not_empty":
return f"NOT {_empty(column)}", []
if args["op"] in ("in", "not_in"):
values = args.get("values")
if not values:
raise ToolError(f"operator {args['op']} needs a list of values")
marks = ", ".join("?" for _ in values)
negate = "NOT " if args["op"] == "not_in" else ""
return f"{column} {negate}IN ({marks})", list(values)
if "value" not in args:
raise ToolError(f"operator {args['op']} needs a value")
if args["op"] == "contains":
return f"CAST({column} AS VARCHAR) ILIKE '%' || ? || '%'", [str(args["value"])]
return f"{column} {COMPARISONS[args['op']]} ?", [args["value"]]
Har bir qiymat ? belgisi o'rniga parametr sifatida beriladi. Ustun nomlari esa q()
funksiyasi bilan qo'shtirnoq ichiga olinadi va ichidagi qo'shtirnoqlar ikkilanadi. Bu nima uchun
kerakligini yomon niyatli qiymat bilan sinaymiz — sintetik kredit jadvalida:
import json from accuracy.run import open_data, BASE from accuracy.execute import run_plan from accuracy.tools import q, ToolError sozlamalar = json.load(open(TEZ / "accuracy/settings.json", encoding="utf-8")) con = open_data(str(DATA / "loans.csv")) yomon = "x'; DROP TABLE base; --" natija = run_plan(con, BASE, [{"tool": "filter", "args": {"column": "filial", "op": "=", "value": yomon}}], sozlamalar) print(f"topilgan qatorlar: {len(natija['rows'])}") print(f"jadval joyida: {con.execute(f'select count(*) from {BASE}').fetchone()[0]:,} qator") print(f"yomon ustun nomi qo'shtirnoq ichida: {q('summa\"; DROP TABLE base; --')}")
topilgan qatorlar: 0 jadval joyida: 20,000 qator yomon ustun nomi qo'shtirnoq ichida: "summa""; DROP TABLE base; --"
Yomon qiymat shunchaki qidiriladigan matn bo'lib qoldi: unday filial yo'q, nol qator, jadval butun. Tez modeli grammar bilan cheklangan bo'lsa ham, engine modelga ishonmaydi — bu ikkinchi to'siqning bir qismi.
Bitta shahar, to'rt yozilish
4-bobda bank fayllari iflos ekanini ko'rdik: bitta shahar bir necha xil yozilgan. Sintetik kredit jadvalidagi filiallar:
show(con.execute(f"select filial, count(*) as qatorlar from {BASE} group by 1 order by 1").df()) oddiy = con.execute(f"select count(*) from {BASE} where filial = 'Toshkent'").fetchone()[0] print(f"oddiy SQL bilan filial = 'Toshkent': {oddiy:,} qator")
oddiy SQL bilan filial = 'Toshkent': 2,218 qator
| filial | qatorlar |
|---|---|
| Andijon | 2 176 |
| Buxoro | 2 158 |
| Namangan | 2 251 |
| Qarshi | 2 209 |
| Samarqand | 2 235 |
| Samarqand | 2 240 |
| Toshkent | 2 218 |
| Бухоро | 2 229 |
| Тошкент | 2 284 |
Toshkent — "Toshkent" va "Тошкент". Samarqand — oxirida bo'sh joy bilan va bo'sh joysiz. Buxoro — lotin va kirill. Oddiy SQL faqat aynan yozilganini topadi va qolganini jim tashlab ketadi. Tahlilchi "Toshkent filiali" deganda, albatta, hammasini nazarda tutadi.
Shuning uchun Tezda = va in amallari imloni hisobga oladi. Tool ustundagi har bir qiymatni
bir xil shaklga keltiradi — kichik harf, kirill harflari lotinga, bo'sh joy va apostroflar
olib tashlanadi — va so'ralgan qiymat bilan mos keladigan hamma yozilishlarni oladi:
for qiymat in ("Toshkent", "Тошкент", "toshkent", "Toshkentt", "Tashkent", "Samarkand", "Bukhara"): topildi = run_plan(con, BASE, [{"tool": "filter", "args": {"column": "filial", "op": "=", "value": qiymat}}], sozlamalar) print(f"filial = {qiymat!r:12s} -> {len(topildi['rows']):,} qator")
filial = 'Toshkent' -> 4,502 qator filial = 'Тошкент' -> 4,502 qator filial = 'toshkent' -> 4,502 qator filial = 'Toshkentt' -> 4,502 qator filial = 'Tashkent' -> 4,502 qator filial = 'Samarkand' -> 4,475 qator filial = 'Bukhara' -> 0 qator
Birinchi uchtasi — bir xil shaklga keladi. "Toshkentt" va "Tashkent" — jadvalda yo'q, lekin bitta harf farq bilan yagona yaqin qiymat bor, va tool o'shani oladi. "Samarkand" ham shunday. "Bukhara" esa "Buxoro" dan juda uzoq — hech narsa topilmaydi, va bu to'g'ri: taxmin qilgandan ko'ra nol qator yaxshiroq.
Bu "eng yaqin yozilish" qoidasi ehtiyotkor qilib qurilgan, sozlamalarda uchta chegara bor:
for kalit in ("spelling_max_edits", "spelling_max_ending", "spelling_min_length"): print(f"{kalit:20s} {sozlamalar[kalit]}")
spelling_max_edits 1 spelling_max_ending 4 spelling_min_length 5
Ko'pi bilan bitta harf farqi (yoki oxiriga qo'shilgan bir necha harf — o'zbekcha qo'shimchalar uchun), kamida besh harfli so'zlar, raqamlari bir xil bo'lishi shart, va yaqin qiymat faqat bitta bo'lsa. Ikkita yaqin qiymat bo'lsa — tool hech birini tanlamaydi. Qisqa kodlar va raqamli qiymatlar (masalan, "ATM-101" va "ATM-102") hech qachon aralashmaydi.
Bitta muhim izoh. Jadvalda qiymatlari to'liq ro'yxat qilingan ustunlarda grammar modelga baribir faqat ro'yxatdagi qiymatni yozishga ruxsat beradi (3-bob). Imlo qoidasi u yerda boshqa ishni qiladi: model "Toshkent" ni tanlasa, "Тошкент" qatorlari ham kiradi. Erkin ustunlarda esa — ya'ni faqat namunalar ko'rsatilgan joyda — model jadvalda yo'q yozilishni ham yozishi mumkin, va u yerda "eng yaqin yozilish" ishga tushadi.
Imloni birlashtirish
Ba'zan tahlilchi filtr emas, tozalash so'raydi: "filial nomlaridagi har xil yozilishlarni
birlashtir". Bu — unify_spelling tooli. U har bir guruh uchun eng ko'p uchraydigan yozilishni
asosiy deb oladi:
birlashgan = run_plan(con, BASE, [ {"tool": "unify_spelling", "args": {"column": "filial"}}, {"tool": "group_and_total", "args": {"group_by": ["filial"], "value_column": "kredit_raqami", "agg": "count"}}, ], sozlamalar) show(pd.DataFrame(sorted(birlashgan["rows"]), columns=birlashgan["columns"]))
| filial | kredit_raqami |
|---|---|
| Andijon | 2 176 |
| Namangan | 2 251 |
| Qarshi | 2 209 |
| Samarqand | 4 475 |
| Бухоро | 4 387 |
| Тошкент | 4 502 |
To'qqizta qiymat oltitaga tushdi, va qatorlar soni yo'qolmadi. Lekin natijaga diqqat bilan qarang: Toshkent va Buxoro guruhlari kirill yozilishda qoldi — "Тошкент" va "Бухоро". Sababi: bu jadvalda ikkalasining kirill yozilishi lotindan biroz ko'proq uchraydi. Qoida "eng ko'p uchragani" — va bu ba'zan foydalanuvchi kutgan yozilish emas. Bu xato emas, lekin ilovada foydalanuvchiga ko'rsatilishi va o'zgartirish imkoni berilishi kerak bo'lgan qaror.
Sanalar
4-bobdagi uchta formatli sana ustuni ham shu yerda tozalanadi. fix_dates sozlamalardagi
formatlarni tartib bilan sinab ko'radi va birinchi mos kelganini oladi:
print("formatlar:", sozlamalar["date_formats"]) run_plan(con, BASE, [{"tool": "fix_dates", "args": {"column": "berilgan_sana"}}], sozlamalar, keep_as="sanalar") jami, sanali, eng_erta, eng_kech = con.execute("select count(*), count(berilgan_sana), min(berilgan_sana), max(berilgan_sana) from sanalar").fetchone() print(f"qatorlar: {jami:,}, sana bo'lgani: {sanali:,}, oraliq: {eng_erta} — {eng_kech}") show(con.execute(f""" select b.berilgan_sana as oldin, strftime(s.berilgan_sana, '%Y-%m-%d') as keyin from {BASE} b join sanalar s using (kredit_raqami) where b.berilgan_sana like '%/%' limit 3 """).df())
formatlar: ['%d.%m.%Y', '%Y-%m-%d', '%d/%m/%Y'] qatorlar: 20,000, sana bo'lgani: 20,000, oraliq: 2024-01-01 — 2026-09-30
| oldin | keyin |
|---|---|
| 20/11/2024 | 2024-11-20 |
| 04/02/2026 | 2026-02-04 |
| 23/04/2026 | 2026-04-23 |
Hamma sana o'qildi. Lekin 04/02/2026 ga qarang: u 4-fevral deb o'qildi, chunki ro'yxatda
"kun/oy/yil" formati bor. O'zbekiston va Rossiyada sana shunday yoziladi. Agar fayl Amerika
formatida (oy/kun/yil) bo'lsa, 4-fevral aslida 2-aprel bo'ladi — va engine buni sezmaydi:
ikkala o'qish ham haqiqiy sana. Bu ochiq xavf: ilova fayl ochilganda bunday ustunni topib,
foydalanuvchidan so'rashi kerak (masalan, 12 dan katta "oy" qiymati bormi yo'qmi — shunga qarab).
Hozircha bunday tekshiruv yo'q.
"Bir xil natija" qanday hukm qilinadi
Endi testlarga o'tamiz. 3-bobda aytdik: model plani ham, to'g'ri plan ham bajariladi va
natijalari solishtiriladi. Solishtiruvchi funksiya — accuracy/compare.py:
from accuracy import compare print(inspect.getsource(compare.canonical_rows)) print(inspect.getsource(compare.same_result))
def canonical_rows(result, settings):
digits = settings["significant_digits"]
return sorted((tuple(_cell(value, digits) for value in row) for row in result["rows"]), key=repr)
def same_result(left, right, settings):
return (
len(left["columns"]) == len(right["columns"])
and _artifacts(left) == _artifacts(right)
and canonical_rows(left, settings) == canonical_rows(right, settings)
)
Uchta narsa tekshiriladi: ustunlar soni, qo'shimcha natijalar (grafik va saralash belgilari)
va qatorlar. Qatorlar solishtirishdan oldin ikki marta "tozalanadi": sonlar 10 ta muhim raqamgacha
yaxlitlanadi (kompyuter hisobidagi 0.1 + 0.2 = 0.30000000000000004 kabi shovqin uchun), va
qatorlar tartiblanadi — ya'ni qatorlar tartibi hisobga olinmaydi.
Tartib-chi? Agar so'rov "saralab ber" bo'lsa? Buning uchun sort tooli qo'shimcha natija sifatida
belgi qaytaradi — qaysi ustun bo'yicha va qaysi yo'nalishda. Belgilar solishtiriladi:
saralangan = run_plan(con, BASE, [{"tool": "sort", "args": {"by": "qoldiq_uzs", "descending": True}}], sozlamalar) teskari = run_plan(con, BASE, [{"tool": "sort", "args": {"by": "qoldiq_uzs", "descending": False}}], sozlamalar) saralanmagan = run_plan(con, BASE, [{"tool": "select_columns", "args": {"columns": saralangan["columns"]}}], sozlamalar) print("belgi:", saralangan["artifacts"]) print("kamayish va o'sish bir xilmi: ", compare.same_result(saralangan, teskari, sozlamalar)) print("saralangan va saralanmagan bir xilmi:", compare.same_result(saralangan, saralanmagan, sozlamalar))
belgi: [{'sort': 'qoldiq_uzs', 'descending': True}]
kamayish va o'sish bir xilmi: False
saralangan va saralanmagan bir xilmi: False
Qatorlar bir xil, lekin belgilar har xil — demak natija har xil. Grafik ham shunday: "ustunli grafik" va "chiziqli grafik" bir xil raqamlar bilan ham farqli javob.
Bu usulning chegaralari
Ikki chegara bor va ularni ochiq yozish kerak.
Birinchi: ustun nomlari solishtirilmaydi, faqat soni. Bu ataylab qilingan — share
qo'shimchasi yoki suhbatdagi nomlar har xil bo'lishi mumkin. Lekin bu shuni anglatadiki, ikki xil
ustun tasodifan bir xil qiymatlarga ega bo'lsa, test farqni sezmaydi. Amalda bu kam uchraydi.
Ikkinchi: 3-bobda aytilgan — ikki xil plan tasodifan bir xil natija berishi mumkin. Masalan, jadvalda faqat bitta valyuta qolgan bo'lsa, valyuta filtri bor va yo'q planlar bir xil chiqadi.
Ikkala holatda ham test ortiqcha yumshoq bo'ladi, qattiq emas: noto'g'ri javobni to'g'ri deb hisoblashi mumkin, teskarisi emas. Demak bizning testdagi raqamlarimiz modelni biroz yaxshiroq ko'rsatishi mumkin, yomonroq emas. Buni esda tutish kerak.
Hukmning oltita turi
Har bir test savoliga model javobi oltita hukmdan birini oladi. Hukm chiqaradigan funksiya —
accuracy/run.py dagi judge:
from accuracy import run print(inspect.getsource(run.judge))
def judge(con, question, answer, expected, settings):
if not isinstance(answer, dict) or not ({"ask", "steps"} & answer.keys()):
return False, "invalid answer"
asked = "ask" in answer
if question["expect"] == "ask":
return asked, "asked" if asked else "guessed instead of asking"
if asked:
return False, "asked on a clear request"
try:
result = run_plan(con, BASE, effective_steps(_earlier(question), answer), settings)
except ToolError as error:
return False, f"plan failed: {error}"
matched = any(same_result(result, reference, settings) for reference in expected[question["id"]])
return matched, "same result" if matched else "different result"
| Hukm | Ma'nosi |
|---|---|
| same result | Plan bajarildi va natija kutilgan bilan bir xil — to'g'ri |
| asked | Savol berish kerak edi va model savol berdi — to'g'ri |
| different result | Plan bajarildi, lekin boshqa natija — xato, va eng xavflisi: tashqaridan to'g'ri ko'rinadi |
| guessed instead of asking | Savol berish kerak edi, model taxmin qildi — xato |
| asked on a clear request | So'rov aniq edi, model ortiqcha savol berdi — xato |
| plan failed | Plan bajarilmadi (masalan, yo'q ustun) — xato, lekin ko'rinadigan |
Bu jadval kitobning qolgan qismida qayta-qayta keladi. Ayniqsa ikki xato turi — "different result" va "guessed instead of asking" — biz uchun hallucination (gallyutsinatsiya) degani: model haqiqatda yo'q narsaga asoslanib, ishonchli ko'rinadigan javob beradi. Raqamni DuckDB hisoblagani uchun Tezda hisob xatosi yo'q. Gallyutsinatsiya Tezda faqat ikki ko'rinishda bo'ladi: noto'g'ri plan yoki berilmagan savol.
Xulosa
- Har bir tool — kichik Python funksiyasi, u SQL yasaydi. Qiymatlar parametr sifatida beriladi, nomlar qo'shtirnoq ichida — yomon niyatli qiymat jadvalga zarar qilmaydi.
=vainimloni hisobga oladi: "Toshkent" deganda "Тошкент" ham kiradi. Bitta harflik xatolar faqat yagona yaqin qiymat bo'lsa tuzatiladi.unify_spellingeng ko'p uchraydigan yozilishni oladi — bu jadvalda Toshkent va Buxoro kirill yozilishda qoldi.- Sanalar "kun/oy/yil" deb o'qiladi; Amerika formatidagi fayl jim xato o'qiladi — ochiq xavf.
- "Bir xil natija": ustunlar soni, belgilar va tartiblangan, yaxlitlangan qatorlar. Test ortiqcha yumshoq bo'lishi mumkin, qattiq emas.
- Tezda gallyutsinatsiya ikki xil: noto'g'ri plan va berilmagan savol.
Keyingi bobda Excel faylning o'zi: loader sarlavhani, jadval ustidagi nomlarni va "Jami" qatorlarini qanday topadi.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.