Asosiy qismga o'tish

Plan generatori: minglab har xil, lekin har doim bajariladigan planlar

13-bob 9 kod bloki 2324 so'z

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

Kod plan yozadi

12-bobda tartibni belgiladik: avval plan, keyin matn. Bu bob — birinchi yarmi haqida: planlarni kim yasaydi va ular qanday qilib har doim to'g'ri bo'ladi.

Javob — oddiy kod: training/plans.py. U hech qanday model ishlatmaydi. U jadvalga qaraydi, qaysi ustun nima ekanini aniqlaydi va tasodifiy, lekin ma'noli planlar tuzadi. Keyin har bir plan DuckDB da bajariladi va natijasi tekshiriladi. Natija bo'sh yoki ma'nosiz bo'lsa — plan tashlanadi.

Bu bobda hamma narsani train qilishda ishlatilgan sintetik jadvallardan biri — bank tranzaksiyalari jadvali (tranzaksiyalar) ustida ko'ramiz. U butunlay o'ylab topilgan.

Birinchi qadam: ustunlarni tanish

Plan tuzishdan oldin generator har bir ustunning turini bilishi kerak. "Filial" bo'yicha guruhlash mumkin, "summa" ni qo'shish mumkin, "tranzaksiya_id" ni qo'shish ma'nosiz. Bu ishni training/profile.py qiladi:

python16 qator
import json
import random
import duckdb
from accuracy.tools import ROW_ID
from training.profile import load_roles, profile_table, with_table_today

sozlamalar = {**json.load(open(TEZ / "accuracy/settings.json", encoding="utf-8")),
              **json.load(open(TEZ / "training/settings.json", encoding="utf-8"))}
con = duckdb.connect()
con.execute(f"create table base as select row_number() over () as {ROW_ID}, * from read_csv('{DATA / 'bank.csv'}')")
print(f"qatorlar: {con.execute('select count(*) from base').fetchone()[0]:,}")

turlar = profile_table(con, "base", sozlamalar, load_roles(TEZ / "prompt/domains/bank_transactions"))
for tur, ustunlar in turlar.items():
    if ustunlar:
        print(f"{tur:12s} {', '.join(ustunlar)}")
Chiqish
qatorlar: 20,000
category     filial, viloyat, mijoz_ismi, mijoz_turi, karta_turi, operatsiya_turi, valyuta, kanal, holat, rad_sababi, qabul_qiluvchi_bank
number       summa_uzs, komissiya, kurs, kredit_qoldigi, muddati_otgan_kun
amount       summa_uzs, komissiya, kredit_qoldigi
amount_text  summa
id           tranzaksiya_id, hisob_raqami
entity       mijoz_id
date         yaratilgan_vaqt
date_text    sana
number_text  summa
has_empty    komissiya, xodim_id, rad_sababi, kredit_id, kredit_qoldigi, izoh
variants     filial
code         vaqt, filial_kodi, terminal_id, xodim_id, kredit_id, mfo, izoh, manba_tizim
person       mijoz_id, mijoz_ismi

Bir ustun bir nechta ro'yxatda bo'lishi mumkin. summa_uzs — ham number, ham amount (pul): uni qo'shish ma'noli. kurs — faqat number: kurslarni qo'shish ma'nosiz, lekin o'rtachasi ma'noli. summa — number_text: son, lekin matn sifatida yozilgan (4-bobdagi "19 113 274,33"), demak u faqat fix_numbers dan keyin ishlatiladi. filial — variants: unda bir xil qiymat har xil yozilgan, demak unify_spelling uchun nomzod. code ro'yxatidagilar — kodlar va identifikatorlar: ularni na guruhlash, na qo'shish kerak.

Ba'zi narsalarni kod o'zi aniqlay olmaydi. Masalan, mijoz_id — oddiy butun son, lekin u odamni bildiradi: "eng ko'p to'lagan mijozlar" degan so'rov aynan shu ustun bo'yicha guruhlaydi. Bunday bilim har bir jadval uchun kichik qo'lda yozilgan faylda turadi — roles.json:

python1 qator
print(json.dumps(json.load(open(TEZ / "prompt/domains/bank_transactions/roles.json", encoding="utf-8")), ensure_ascii=False))
Chiqish
{"amount": ["summa", "summa_uzs", "komissiya", "kredit_qoldigi"], "measure": ["kurs", "muddati_otgan_kun"], "dimension": ["filial", "viloyat", "mijoz_turi", "karta_turi", "operatsiya_turi", "valyuta", "kanal", "holat", "rad_sababi", "qabul_qiluvchi_bank"], "person": ["mijoz_id", "mijoz_ismi"], "code": ["vaqt", "filial_kodi", "terminal_id", "xodim_id", "kredit_id", "mfo", "izoh", "manba_tizim"]}

Plan shakllari

Generator planni "shakl" bo'yicha tuzadi: faqat filtr, filtr va jamlash, tozalash va jamlash, jamlash va grafik, va hokazo. Har bir shaklning og'irligi sozlamalarda — ya'ni qaysi turdagi planlar ko'proq chiqishi kodda emas, sozlamada belgilanadi:

python4 qator
gen = sozlamalar["generator"]
print("shakllar:", gen["shapes"])
print("jamlashlar:", gen["aggregates"])
print("filtr amallari:", gen["category_conditions"])
Chiqish
shakllar: {'clean': 3, 'condition': 3, 'summary': 4, 'conditions_summary': 5, 'summary_chart': 2, 'chart': 1, 'clean_summary': 2, 'summary_export': 1, 'summary_sort': 2, 'condition_select': 1}
jamlashlar: {'sum': 5, 'avg': 2, 'min': 1, 'max': 1, 'count': 2, 'count_distinct': 1}
filtr amallari: {'one': {'filter': {'=': 4, '!=': 1, 'contains': 0.5}, 'remove_rows': {'=': 3, 'contains': 0.5}}, 'many': {'filter': {'in': 4, 'not_in': 1}, 'remove_rows': {'in': 3}}}

Masalan, "conditions_summary" — bir yoki ikkita shart va oxirida jamlash: "Toshkentdagi bajarilgan tranzaksiyalarni valyuta bo'yicha jamla". Bu eng ko'p og'irlikka ega shakl, chunki tahlilchining odatiy savoli shunday.

Planlar yasash

Endi uch yuzta plan yasaymiz va tekshiramiz — xuddi training ma'lumoti yasalgandek:

python17 qator
from collections import Counter
from training.check import keep_plan
from training.plans import generate_plans
from training.describe import describe_plan

sozlamalar = with_table_today(con, "base", turlar, sozlamalar)
rejalar = generate_plans(con, "base", turlar, sozlamalar, random.Random(1), 300)
saqlangan = [reja for reja in rejalar if keep_plan(con, "base", reja, sozlamalar)]
print(f"yasalgan: {len(rejalar)}, tekshiruvdan o'tgan: {len(saqlangan)}")
print("qadamlar soni:", dict(sorted(Counter(len(r["steps"]) for r in saqlangan).items())))
print("asboblar:", dict(Counter(q["tool"] for r in saqlangan for q in r["steps"]).most_common()))

reja_matni = json.load(open(TEZ / "training/plan_text.json", encoding="utf-8"))
print()
for reja in saqlangan[:4]:
    print(describe_plan(reja, reja_matni))
    print()
Chiqish
yasalgan: 300, tekshiruvdan o'tgan: 285
qadamlar soni: {1: 112, 2: 138, 3: 28, 4: 7}
asboblar: {'group_and_total': 128, 'filter': 81, 'top_n': 62, 'remove_rows': 50, 'make_chart': 40, 'period_compare': 30, 'sort': 25, 'filter_dates': 21, 'export_xlsx': 16, 'fill_empty': 11, 'remove_duplicates': 10, 'select_columns': 10, 'fix_dates': 9, 'unify_spelling': 4, 'fix_numbers': 3}

1. Delete the rows where kredit_qoldigi is at most 44000000; all other rows stay.

1. Convert sana to real dates.
2. For each quarter, compute the total of komissiya by sana, and compare it with the same quarter of the previous year.

1. For each month, compute the total of komissiya by yaratilgan_vaqt, and compare it with the previous month.

1. For each mijoz_turi, compute the number of different values of mijoz_ismi.
2. Sort the rows by mijoz_ismi, highest first.

Har bir plan — jadvalning haqiqiy ustunlari va haqiqiy qiymatlaridan. Filtrdagi "Bajarildi" yoki "Toshkent" — o'ylab topilmagan, jadvaldan olingan. Bu muhim: model training paytida faqat haqiqatan bor qiymatlarni ko'radi.

Va yana bir xususiyat — bir xil seed har doim bir xil planlarni beradi:

python2 qator
qayta = generate_plans(con, "base", turlar, sozlamalar, random.Random(1), 300)
print("bir xil seed — bir xil rejalar:", qayta == rejalar)
Chiqish
bir xil seed — bir xil rejalar: True

Bu kichik narsa emas. Xato topilganda uni aynan qaytarish mumkin, va training ma'lumoti tasodifan o'zgarib qolmaydi.

Tekshiruv: plan natija berishi kerak

Planning sintaksisi to'g'ri bo'lishi yetarli emas. "Bekor qilingan va bajarilgan tranzaksiyalar" — ikkita qarama-qarshi filtr — bo'sh natija beradi. Bunday plandan model hech narsa o'rganmaydi, yoki yomoni — ma'nosiz narsani o'rganadi.

Tekshiruvchi — training/check.py dagi keep_plan:

python5 qator
import inspect
from training import check

print(inspect.getsource(check.keep_plan))
print("chegaralar:", sozlamalar["keep"])
Chiqish
def keep_plan(con, table, plan, settings):
    keep = settings["keep"]
    steps = plan["steps"]
    try:
        counts = [_rows(con, table, steps[:index], settings) for index in range(len(steps) + 1)]
        if 0 in counts:
            return False
        for index, step in enumerate(steps):
            if step["tool"] in CONDITIONS and not (keep["min_share"] <= counts[index + 1] / counts[index] <= keep["max_share"]):
                return False
    except ToolError:
        return False
    return True

chegaralar: {'min_share': 0.01, 'max_share': 0.99}

Ikki qoida. Birinchi: planning hech bir bosqichi bo'sh natija bermasin. Ikkinchi: har bir filtr qatorlarning kamida 1 foizini va ko'pi bilan 99 foizini qoldirsin — ya'ni hech narsani o'zgartirmaydigan filtr ham, deyarli hammasini o'chiradigan filtr ham olinmaydi. Tashlangan planlarni ko'raylik:

python10 qator
from accuracy.execute import run_plan

for reja in [r for r in rejalar if r not in saqlangan][:4]:
    try:
        sonlar = [len(run_plan(con, "base", reja["steps"][:i], sozlamalar)["rows"]) for i in range(len(reja["steps"]) + 1)]
        print("qatorlar har bosqichda:", sonlar)
    except Exception as xato:
        print("bajarilmadi:", str(xato)[:80])
    print(describe_plan(reja, reja_matni))
    print()
Chiqish
qatorlar har bosqichda: [20000, 0, 0, 0, 0]
1. Keep only the rows where yaratilgan_vaqt is in the previous quarter.
2. Keep only the rows where komissiya is at least 12000.
3. Keep only the rows where komissiya is at most 25000.
4. For each mijoz_ismi, compute the average of kurs, then show the 20 with the highest result.

qatorlar har bosqichda: [20000, 5094, 5094, 3170, 1]
1. Keep only the rows where mijoz_turi is 'Yuridik'.
2. Keep only the rows where muddati_otgan_kun is at least 0.
3. Keep only the rows where muddati_otgan_kun is at most 5.
4. Compute the number of rows of tranzaksiya_id over all rows, as one number.

qatorlar har bosqichda: [20000, 10062, 10062, 1]
1. Keep only the rows where kurs is at least 150.
2. Keep only the rows where kurs is at most 14000.
3. Compute the total of komissiya over all rows, as one number.

qatorlar har bosqichda: [20000, 20000, 13327, 7]
1. Keep only the rows where kurs is less than 14000.
2. Keep only the rows where komissiya is at most 37000.
3. For each quarter, compute the total of kredit_qoldigi by sana, and compare it with the previous quarter.

Tashlanish sababi ko'rinib turibdi: filtr deyarli hamma qatorni qoldirgan yoki bosqich bo'sh natija bergan.

Tekshiruvning o'zi ham xato qilgan

Bu tekshiruvning birinchi versiyasida xato bor edi. Butun jadval bo'yicha jami (group_and_total guruhsiz) bo'sh jadval ustida ham bitta qator qaytaradi — jami nol. Tekshiruv "bitta qator bor" deb o'tkazib yuborgan va bo'linishda nolga duch kelgan. Natijada yomon planlar o'tib ketgan. Tuzatish — yuqoridagi "har bir bosqich" qoidasi: endi filtrdan keyingi bo'sh natija jamidan oldin ushlanadi. Xatolar jurnalida bu 9-raqam, va undan qoida chiqdi: har bir yangi tool shakli bo'sh natija bilan test qilinadi.

Bugun qo'shilgan uchta qoida

Bu kitob yozilayotgan kuni generatorga uchta o'zgarish kiritildi. Uchalasi ham test natijalarini tahlil qilish paytida topilgan kamchiliklardan (6-qism):

1. contains faqat qiymatning bir qismi bilan. Avval generator contains amalini bitta so'zli qiymat bilan ham ishlatardi — masalan filial contains "Toshkent", garchi filial = "Toshkent" aniqroq bo'lsa ham. tez8 ning training ma'lumotida contains li planlarning qariyb yarmi shunday edi, va model "aniq qiymatni bilmasam, foydalanuvchining so'zini contains bilan qidiraman" degan odatni o'rgandi. Endi contains faqat ko'p so'zli qiymatning bir qismi bilan yasaladi:

python5 qator
qismlar = [q["args"]["value"] for r in saqlangan for q in r["steps"] if q.get("args", {}).get("op") == "contains"]
butun = [q for r in saqlangan for q in r["steps"] if q.get("args", {}).get("op") == "contains"
         and q["args"]["value"] in {str(v) for (v,) in con.execute(f"select distinct {q['args']['column']} from base").fetchall()}]
print(f"contains qadamlari: {len(qismlar)}, ulardan butun qiymat bilan: {len(butun)}")
print("misollar:", qismlar[:5])
Chiqish
contains qadamlari: 4, ulardan butun qiymat bilan: 0
misollar: ['bank', 'qilindi', 'Tursunov', 'Bekor']

2. Soni bo'yicha reyting. "Eng ko'p tranzaksiya qilgan 10 ta mijoz" — bu summa bo'yicha emas, qatorlar soni bo'yicha reyting. Qamrov darvozasi (16-bob) tez8 ning ma'lumotida bunday plan nol ekanini topdi. Endi reytinglarning bir qismi shunday yasaladi:

python5 qator
reyting = [q for r in saqlangan for q in r["steps"] if q["tool"] == "top_n" and "group" in q["args"]]
soni = [q for q in reyting if q["args"]["group"]["agg"] == "count"]
print(f"guruhli reytinglar: {len(reyting)}, ulardan soni bo'yicha: {len(soni)}")
if soni:
    print(describe_plan({"steps": [soni[0]]}, reja_matni))
Chiqish
guruhli reytinglar: 37, ulardan soni bo'yicha: 10
1. For each rad_sababi, compute the number of rows of tranzaksiya_id, then show the 3 with the lowest result.

3. O'zini sanaydigan guruh yo'q. "Har bir mijoz bo'yicha har xil mijozlar soni" — ma'nosiz plan: har bir guruhda javob har doim 1. Generator avval bunday planlarni yasay olardi (xatolar jurnalida 8-raqam: training ma'lumotiga 24 ta shunday qator kirgan). Endi ustun bir vaqtda ham guruh, ham sanaladigan qiymat bo'la olmaydi — va bu qoida test bilan qotirilgan.

Muhim: bu o'zgarishlar hali hech bir o'qitilgan modelga kirmagan. tez8, tez9 va hozir o'qitilayotgan tez10 eski generator bilan yasalgan ma'lumotda o'qitilgan. Yangi qoidalarning foydasi keyingi ma'lumot partiyasida va keyingi modelda o'lchanadi.

Generator nimani qila olmaydi

Uchta chegara bor:

  • Faqat bitta jadval. Generator ikki faylni birlashtirish yoki solishtirish planini yasamaydi — chunki Tezda bunday tool yo'q.
  • Faqat toollar qila oladigan narsa. "Kreditning depozitga nisbati" — ikki ustunni bo'lish — hozircha hech bir toolda yo'q, demak generator ham yasamaydi. Bunday so'rovga model "qila olmayman" deyishi kerak (17-bob).
  • Faqat jadval ko'rsatgan narsa. Generator so'rov matnini bilmaydi — u faqat planni yasaydi. So'rov qanchalik tabiiy chiqishi — o'qituvchiga bog'liq (12-bob), va bu yerda eng katta muammo uzunlik edi.

Xulosa

  • Planlarni model emas, kod yasaydi: jadvalning haqiqiy ustunlari va qiymatlaridan, sozlamadagi shakl og'irliklari bo'yicha.
  • Ustun turlari avtomatik aniqlanadi; kod bilmaydigan ma'no (masalan, "bu ustun — odam") kichik qo'lda yozilgan faylda turadi.
  • Har bir plan bajariladi va tekshiriladi: hech bir bosqich bo'sh bo'lmasin, har bir filtr ma'noli ulushni qoldirsin.
  • Bir xil seed — bir xil planlar.
  • Bugun uchta qoida qo'shildi (contains, soni bo'yicha reyting, o'zini sanaydigan guruh); ular hali hech bir modelda sinalmagan.

Keyingi bobda ikkinchi yarmi: o'qituvchi yozgan matn haqiqatan shu planni bildiradimi — buni qanday tekshiramiz.

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar