Plan generatori: minglab har xil, lekin har doim bajariladigan planlar
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:
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)}")
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:
print(json.dumps(json.load(open(TEZ / "prompt/domains/bank_transactions/roles.json", encoding="utf-8")), ensure_ascii=False))
{"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:
gen = sozlamalar["generator"] print("shakllar:", gen["shapes"]) print("jamlashlar:", gen["aggregates"]) print("filtr amallari:", gen["category_conditions"])
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:
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()
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:
qayta = generate_plans(con, "base", turlar, sozlamalar, random.Random(1), 300) print("bir xil seed — bir xil rejalar:", qayta == rejalar)
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:
import inspect from training import check print(inspect.getsource(check.keep_plan)) print("chegaralar:", sozlamalar["keep"])
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:
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()
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:
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])
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:
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))
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.