| | 1 | = Напредни извештаи = |
| | 2 | |
| | 3 | Извештаите се извршуваат со SET search_path TO project; Двата извештаи се во прикачената датотека [attachment:advanced_reports.sql]. |
| | 4 | |
| | 5 | Во релационата алгебра се користи проширената нотација: σ (селекција), π (генерализирана проекција, со пресметани атрибути), ⋈ (спојување), ⟕ (лево надворешно спојување), ρ (преименување), γ (групирање и агрегатни функции), τ (подредување) и ← (доделување на привремена релација). |
| | 6 | |
| | 7 | == Квартален учинок во решавањето на пријавите по категорија == |
| | 8 | |
| | 9 | Општината сака да знае колку ефикасно ги решава различните видови комунални проблеми и дали се подобрува со текот на времето. Извештајот за секој квартал и за секоја категорија прикажува: вкупен број на пријави, број на решени и одбиени пријави, процент на решени, просечно време до првиот одговор (од поднесување до статус „примена“, во часови), просечно време до решавање (во денови), промена на времето до решавање во однос на претходниот квартал, и рангирање на категориите во кварталот од најбавната кон најбрзата. Со извештајот се откриваат категориите каде е потребно повеќе ресурси, и се следи долгорочниот тренд на квартално, полугодишно и годишно ниво. |
| | 10 | |
| | 11 | === Решение во SQL === |
| | 12 | |
| | 13 | {{{#!sql |
| | 14 | WITH report_times AS ( |
| | 15 | SELECT r.report_id, r.category_id, r.status, r.created_at, |
| | 16 | date_trunc('quarter', r.created_at) AS quarter, |
| | 17 | MIN(l.changed_at) FILTER (WHERE l.status = 'received') AS received_at, |
| | 18 | MIN(l.changed_at) FILTER (WHERE l.status = 'resolved') AS resolved_at |
| | 19 | FROM reports r |
| | 20 | JOIN status_logs l ON l.report_id = r.report_id |
| | 21 | GROUP BY r.report_id |
| | 22 | ), |
| | 23 | per_quarter AS ( |
| | 24 | SELECT quarter, category_id, |
| | 25 | COUNT(*) AS total, |
| | 26 | COUNT(*) FILTER (WHERE status = 'resolved') AS resolved, |
| | 27 | COUNT(*) FILTER (WHERE status = 'rejected') AS rejected, |
| | 28 | AVG(EXTRACT(EPOCH FROM received_at - created_at) / 3600) AS avg_response_hours, |
| | 29 | AVG(EXTRACT(EPOCH FROM resolved_at - created_at) / 86400) AS avg_resolution_days |
| | 30 | FROM report_times |
| | 31 | GROUP BY quarter, category_id |
| | 32 | ) |
| | 33 | SELECT to_char(pq.quarter, 'YYYY-"Q"Q') AS quarter, |
| | 34 | c.name AS category, |
| | 35 | pq.total, pq.resolved, pq.rejected, |
| | 36 | ROUND(100.0 * pq.resolved / pq.total, 1) AS resolved_pct, |
| | 37 | ROUND(pq.avg_response_hours, 1) AS avg_response_hours, |
| | 38 | ROUND(pq.avg_resolution_days, 1) AS avg_resolution_days, |
| | 39 | ROUND(pq.avg_resolution_days - prev.avg_resolution_days, 1) AS change_vs_prev_quarter, |
| | 40 | RANK() OVER (PARTITION BY pq.quarter |
| | 41 | ORDER BY pq.avg_resolution_days DESC NULLS LAST) AS slowest_rank |
| | 42 | FROM per_quarter pq |
| | 43 | JOIN categories c ON c.category_id = pq.category_id |
| | 44 | LEFT JOIN per_quarter prev |
| | 45 | ON prev.category_id = pq.category_id |
| | 46 | AND prev.quarter = pq.quarter - INTERVAL '3 months' |
| | 47 | ORDER BY pq.quarter, slowest_rank; |
| | 48 | }}} |
| | 49 | |
| | 50 | === Решение во релациона алгебра === |
| | 51 | |
| | 52 | {{{ |
| | 53 | Rec ← report_id γ MIN(changed_at)→received_at ( σ status='received' (status_logs) ) |
| | 54 | |
| | 55 | Res ← report_id γ MIN(changed_at)→resolved_at ( σ status='resolved' (status_logs) ) |
| | 56 | |
| | 57 | RT ← π report_id, category_id, created_at, |
| | 58 | quarter(created_at)→quarter, |
| | 59 | received_at, resolved_at, |
| | 60 | (1 if status='resolved' else 0)→is_resolved, |
| | 61 | (1 if status='rejected' else 0)→is_rejected |
| | 62 | ( reports ⟕ Rec ⟕ Res ) |
| | 63 | |
| | 64 | PQ ← quarter, category_id γ COUNT(report_id)→total, |
| | 65 | SUM(is_resolved)→resolved, |
| | 66 | SUM(is_rejected)→rejected, |
| | 67 | AVG(received_at − created_at)→avg_response, |
| | 68 | AVG(resolved_at − created_at)→avg_resolution |
| | 69 | ( RT ) |
| | 70 | |
| | 71 | Prev ← ρ Prev(prev_of, category_id, prev_avg) |
| | 72 | ( π quarter + 3 months, category_id, avg_resolution (PQ) ) |
| | 73 | |
| | 74 | PQK ← π quarter, category_id, COALESCE(avg_resolution, −1)→sort_key (PQ) |
| | 75 | |
| | 76 | Rank ← a.quarter, a.category_id γ COUNT(b.category_id)→slower_count |
| | 77 | ( ρ a(PQK) ⟕ a.quarter = b.quarter ∧ b.sort_key > a.sort_key ρ b(PQK) ) |
| | 78 | |
| | 79 | Result ← τ quarter, slowest_rank |
| | 80 | ( π quarter, name, total, resolved, rejected, |
| | 81 | 100 · resolved / total→resolved_pct, |
| | 82 | avg_response, avg_resolution, |
| | 83 | avg_resolution − prev_avg→change_vs_prev_quarter, |
| | 84 | 1 + slower_count→slowest_rank |
| | 85 | ( ( PQ ⋈ Rank ⋈ categories ) |
| | 86 | ⟕ PQ.quarter = Prev.prev_of ∧ PQ.category_id = Prev.category_id Prev ) ) |
| | 87 | }}} |
| | 88 | |
| | 89 | Рангот (RANK) е изразен преку лево надворешно самоспојување: рангот на една категорија е 1 плус бројот на категории во истиот квартал со подолго време на решавање. Категориите без решени пријави добиваат клуч −1, што одговара на NULLS LAST. |
| | 90 | |
| | 91 | == Жаришта на повторувачки проблеми == |
| | 92 | |
| | 93 | Општината сака да ги открие локациите каде истиот вид проблем постојано се повторува, бидејќи тоа укажува на потреба од трајно решение (на пример реконструкција на улица, дополнителни контејнери или замена на инсталации) наместо постојани интервенции. Градот се дели на мрежа од ќелии со големина околу 500 метри (координатите се заокружуваат на 0,005 степени). За секоја ќелија и категорија се пресметува: број на пријави, број на различни граѓани кои пријавиле, број на различни квартали во кои има пријави, број на моментално отворени пријави, просечно време на решавање и датум на последната пријава. Се прикажуваат само ќелиите со најмалку 3 пријави во најмалку 2 различни квартали, рангирани според бројот на пријави помножен со бројот на квартали. Одбиените пријави не се сметаат. Извештајот се користи на годишно и повеќегодишно ниво за планирање на инвестициите. |
| | 94 | |
| | 95 | === Решение во SQL === |
| | 96 | |
| | 97 | {{{#!sql |
| | 98 | WITH resolution AS ( |
| | 99 | SELECT report_id, MIN(changed_at) FILTER (WHERE status = 'resolved') AS resolved_at |
| | 100 | FROM status_logs |
| | 101 | GROUP BY report_id |
| | 102 | ), |
| | 103 | located AS ( |
| | 104 | SELECT r.report_id, r.category_id, r.citizen_id, r.status, r.created_at, r.location_text, |
| | 105 | ROUND(r.latitude / 0.005) * 0.005 AS cell_lat, |
| | 106 | ROUND(r.longitude / 0.005) * 0.005 AS cell_lon, |
| | 107 | res.resolved_at |
| | 108 | FROM reports r |
| | 109 | JOIN resolution res ON res.report_id = r.report_id |
| | 110 | WHERE r.latitude IS NOT NULL |
| | 111 | AND r.status <> 'rejected' |
| | 112 | ) |
| | 113 | SELECT RANK() OVER (ORDER BY COUNT(*) * COUNT(DISTINCT date_trunc('quarter', l.created_at)) DESC) AS hotspot_rank, |
| | 114 | l.cell_lat, l.cell_lon, |
| | 115 | c.name AS category, |
| | 116 | MAX(l.location_text) AS example_address, |
| | 117 | COUNT(*) AS reports, |
| | 118 | COUNT(DISTINCT l.citizen_id) AS distinct_citizens, |
| | 119 | COUNT(DISTINCT date_trunc('quarter', l.created_at)) AS quarters_with_reports, |
| | 120 | COUNT(*) FILTER (WHERE l.status NOT IN ('resolved', 'rejected')) AS open_now, |
| | 121 | ROUND(AVG(EXTRACT(EPOCH FROM l.resolved_at - l.created_at) / 86400), 1) AS avg_resolution_days, |
| | 122 | to_char(MAX(l.created_at), 'DD.MM.YYYY') AS last_report |
| | 123 | FROM located l |
| | 124 | JOIN categories c ON c.category_id = l.category_id |
| | 125 | GROUP BY l.cell_lat, l.cell_lon, c.name |
| | 126 | HAVING COUNT(*) >= 3 |
| | 127 | AND COUNT(DISTINCT date_trunc('quarter', l.created_at)) >= 2 |
| | 128 | ORDER BY hotspot_rank, reports DESC; |
| | 129 | }}} |
| | 130 | |
| | 131 | === Решение во релациона алгебра === |
| | 132 | |
| | 133 | {{{ |
| | 134 | Res ← report_id γ MIN(changed_at)→resolved_at ( σ status='resolved' (status_logs) ) |
| | 135 | |
| | 136 | L ← π report_id, category_id, citizen_id, created_at, location_text, |
| | 137 | round(latitude / 0.005) · 0.005→cell_lat, |
| | 138 | round(longitude / 0.005) · 0.005→cell_lon, |
| | 139 | quarter(created_at)→quarter, |
| | 140 | resolved_at, |
| | 141 | (1 if status ∉ {'resolved','rejected'} else 0)→is_open |
| | 142 | ( σ latitude ≠ NULL ∧ status ≠ 'rejected' (reports) ⟕ Res ) |
| | 143 | |
| | 144 | G ← cell_lat, cell_lon, category_id |
| | 145 | γ COUNT(report_id)→reports, |
| | 146 | COUNT-DISTINCT(citizen_id)→distinct_citizens, |
| | 147 | COUNT-DISTINCT(quarter)→quarters_with_reports, |
| | 148 | SUM(is_open)→open_now, |
| | 149 | AVG(resolved_at − created_at)→avg_resolution, |
| | 150 | MAX(location_text)→example_address, |
| | 151 | MAX(created_at)→last_report |
| | 152 | ( L ) |
| | 153 | |
| | 154 | H ← π cell_lat, cell_lon, category_id, reports, distinct_citizens, |
| | 155 | quarters_with_reports, open_now, avg_resolution, example_address, |
| | 156 | last_report, reports · quarters_with_reports→score |
| | 157 | ( σ reports ≥ 3 ∧ quarters_with_reports ≥ 2 (G) ) |
| | 158 | |
| | 159 | Rank ← a.cell_lat, a.cell_lon, a.category_id γ COUNT(b.category_id)→higher_count |
| | 160 | ( ρ a(H) ⟕ b.score > a.score ρ b(H) ) |
| | 161 | |
| | 162 | Result ← τ hotspot_rank, reports desc |
| | 163 | ( π 1 + higher_count→hotspot_rank, cell_lat, cell_lon, name, |
| | 164 | example_address, reports, distinct_citizens, |
| | 165 | quarters_with_reports, open_now, avg_resolution, last_report |
| | 166 | ( H ⋈ Rank ⋈ categories ) ) |
| | 167 | }}} |
| | 168 | |
| | 169 | Во SQL решението групирањето е според името на категоријата, а во релационата алгебра според category_id. Двете се еквивалентни, бидејќи името на категоријата е единствено (category_name → category_id). |
| | 170 | |
| | 171 | == Користење на вештачка интелигенција == |
| | 172 | |
| | 173 | [wiki:AdvancedReportsAIUsage Користење на вештачка интелигенција за напредните извештаи] |