Asosiy qismga o'tish

DuckDB engine va "bir xil natija"

10-bob 10 kod bloki 2202 so'z

Bu bobdagi kod shu fayllar bilan ishlaydio'z kompyuteringizda ishga tushiring

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:

python4 qator
import inspect
from accuracy import tools

print(inspect.getsource(tools._condition))
Chiqish
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:

python13 qator
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; --')}")
Chiqish
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:

python3 qator
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")
Chiqish
oddiy SQL bilan filial = 'Toshkent': 2,218 qator
filialqatorlar
Andijon2 176
Buxoro2 158
Namangan2 251
Qarshi2 209
Samarqand2 235
Samarqand 2 240
Toshkent2 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:

python3 qator
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")
Chiqish
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:

python2 qator
for kalit in ("spelling_max_edits", "spelling_max_ending", "spelling_min_length"):
    print(f"{kalit:20s} {sozlamalar[kalit]}")
Chiqish
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:

python5 qator
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"]))
filialkredit_raqami
Andijon2 176
Namangan2 251
Qarshi2 209
Samarqand4 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:

python10 qator
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())
Chiqish
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
oldinkeyin
20/11/20242024-11-20
04/02/20262026-02-04
23/04/20262026-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:

python4 qator
from accuracy import compare

print(inspect.getsource(compare.canonical_rows))
print(inspect.getsource(compare.same_result))
Chiqish
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:

python6 qator
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))
Chiqish
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:

python3 qator
from accuracy import run

print(inspect.getsource(run.judge))
Chiqish
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.
  • = va in imloni hisobga oladi: "Toshkent" deganda "Тошкент" ham kiradi. Bitta harflik xatolar faqat yagona yaqin qiymat bo'lsa tuzatiladi.
  • unify_spelling eng 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.

Telegramda yozish Narxlar