feat: Add crime data JSON and district center data for crime incidents

feat: Update database schema to allow nullable year in crimes table

feat: Create test table for future use in migrations

feat: Add crime_cleared column to crimes table and drop test table

feat: Add city_id column to units table and adjust district_id constraints

feat: Add phone column to units table

feat: Add avg_crime column to crimes table

feat: Implement seeder for crime incidents with detailed mock data generation

feat: Add trigger and function for calculating distance to district's police unit
This commit is contained in:
vergiLgood1 2025-05-12 08:17:26 +07:00
parent e422c59da9
commit 77c865958a
26 changed files with 280667 additions and 3726 deletions

View File

@ -1,4 +1,6 @@
export class CRegex {
static readonly PHONE_REGEX = /^(\+62|62|0)[8][1-9][0-9]{6,11}$/;
static readonly BAN_DURATION_REGEX = /^(\d+(ns|us|µs|ms|s|m|h)|none)$/;
}
static readonly CR_YEAR_SEQUENCE = /(\d{4})(?=-\d{4}$)/;
static readonly CR_SEQUENCE_END = /(\d{4})$/;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +1,32 @@
district_id,district_name,crime_total,crime_cleared,avg_crime,avg_score,level
350901,Jombang,41,41,8.2,99,low
350902,Kencong,38,37,7.6,100,low
350903,Sumberbaru,400,332,80.0,26,medium
350904,Gumukmas,401,337,80.2,26,medium
350905,Umbulsari,45,44,9.0,98,low
350906,Tanggul,388,337,77.6,28,medium
350907,Semboro,49,47,9.8,98,low
350908,Puger,448,385,89.6,16,medium
350909,Bangsalsari,473,403,94.6,11,medium
350910,Balung,524,422,104.8,0,medium
350911,Wuluhan,458,379,91.6,14,medium
350912,Ambulu,442,366,88.4,17,medium
350913,Rambipuji,426,373,85.2,21,medium
350914,Panti,45,43,9.0,98,low
350915,Sukorambi,45,44,9.0,98,low
350916,Jenggawah,438,362,87.6,18,medium
350917,Ajung,429,363,85.8,20,medium
350918,Tempurejo,147,143,29.4,78,low
350919,Kaliwates,476,400,95.2,10,high
350920,Patrang,508,416,101.6,4,high
350921,Sumbersari,422,359,84.4,21,high
350922,Arjasa,57,56,11.4,96,low
350923,Mumbulsari,35,35,7.0,100,low
350924,Pakusari,52,52,10.4,97,low
350925,Jelbuk,42,41,8.4,99,low
350926,Mayang,51,51,10.2,97,low
350927,Kalisat,45,45,9.0,98,low
350928,Ledokombo,47,45,9.4,98,low
350929,Sukowono,46,46,9.2,98,low
350930,Silo,433,381,86.6,19,medium
350931,Sumberjambe,43,43,8.6,99,low
district_id,district_name,crime_total,crime_cleared,avg_crime,score,level
350901,Jombang,112,41,10.25,99,low
350902,Kencong,95,37,9.5,100,low
350903,Sumberbaru,458,332,100,26,medium
350904,Gumukmas,456,337,100.25,26,medium
350905,Umbulsari,124,44,11.25,98,low
350906,Tanggul,501,337,97,28,medium
350907,Semboro,127,47,12.25,98,low
350908,Puger,553,385,112,16,medium
350909,Bangsalsari,568,403,118.25,11,medium
350910,Balung,582,422,131,0,medium
350911,Wuluhan,520,379,114.5,14,medium
350912,Ambulu,534,366,110.5,17,medium
350913,Rambipuji,500,373,106.5,21,medium
350914,Panti,135,43,11.25,98,low
350915,Sukorambi,134,44,11.25,98,low
350916,Jenggawah,515,362,109.5,18,medium
350917,Ajung,516,363,107.25,20,medium
350918,Tempurejo,208,143,36.75,78,low
350919,Kaliwates,547,400,119,10,high
350920,Patrang,574,416,127,4,high
350921,Sumbersari,529,359,105.5,21,high
350922,Arjasa,156,56,14.25,96,low
350923,Mumbulsari,103,35,8.75,100,low
350924,Pakusari,113,52,13,97,low
350925,Jelbuk,144,41,10.5,99,low
350926,Mayang,141,51,12.75,97,low
350927,Kalisat,112,45,11.25,98,low
350928,Ledokombo,118,45,11.75,98,low
350929,Sukowono,112,46,11.5,98,low
350930,Silo,522,381,108.25,19,medium
350931,Sumberjambe,141,43,10.75,99,low

1 district_id district_name crime_total crime_cleared avg_crime avg_score score level
2 350901 Jombang 41 112 41 8.2 10.25 99 low
3 350902 Kencong 38 95 37 7.6 9.5 100 low
4 350903 Sumberbaru 400 458 332 80.0 100 26 medium
5 350904 Gumukmas 401 456 337 80.2 100.25 26 medium
6 350905 Umbulsari 45 124 44 9.0 11.25 98 low
7 350906 Tanggul 388 501 337 77.6 97 28 medium
8 350907 Semboro 49 127 47 9.8 12.25 98 low
9 350908 Puger 448 553 385 89.6 112 16 medium
10 350909 Bangsalsari 473 568 403 94.6 118.25 11 medium
11 350910 Balung 524 582 422 104.8 131 0 medium
12 350911 Wuluhan 458 520 379 91.6 114.5 14 medium
13 350912 Ambulu 442 534 366 88.4 110.5 17 medium
14 350913 Rambipuji 426 500 373 85.2 106.5 21 medium
15 350914 Panti 45 135 43 9.0 11.25 98 low
16 350915 Sukorambi 45 134 44 9.0 11.25 98 low
17 350916 Jenggawah 438 515 362 87.6 109.5 18 medium
18 350917 Ajung 429 516 363 85.8 107.25 20 medium
19 350918 Tempurejo 147 208 143 29.4 36.75 78 low
20 350919 Kaliwates 476 547 400 95.2 119 10 high
21 350920 Patrang 508 574 416 101.6 127 4 high
22 350921 Sumbersari 422 529 359 84.4 105.5 21 high
23 350922 Arjasa 57 156 56 11.4 14.25 96 low
24 350923 Mumbulsari 35 103 35 7.0 8.75 100 low
25 350924 Pakusari 52 113 52 10.4 13 97 low
26 350925 Jelbuk 42 144 41 8.4 10.5 99 low
27 350926 Mayang 51 141 51 10.2 12.75 97 low
28 350927 Kalisat 45 112 45 9.0 11.25 98 low
29 350928 Ledokombo 47 118 45 9.4 11.75 98 low
30 350929 Sukowono 46 112 46 9.2 11.5 98 low
31 350930 Silo 433 522 381 86.6 108.25 19 medium
32 350931 Sumberjambe 43 141 43 8.6 10.75 99 low

View File

@ -0,0 +1,32 @@
district_id,district_name,crime_total,crime_cleared,avg_crime,score,level
350901,Jombang,118,110,29.5,73,low
350902,Kencong,91,74,22.75,80,low
350903,Sumberbaru,157,130,39.25,64,high
350904,Gumukmas,91,78,22.75,80,low
350905,Umbulsari,115,88,28.75,74,low
350906,Tanggul,266,213,66.5,39,high
350907,Semboro,94,89,23.5,79,low
350908,Puger,180,160,45,59,high
350909,Bangsalsari,154,132,38.5,65,high
350910,Balung,278,223,69.5,37,high
350911,Wuluhan,216,176,54,51,high
350912,Ambulu,157,124,39.25,64,high
350913,Rambipuji,278,170,69.5,37,low
350914,Panti,139,109,34.75,69,low
350915,Sukorambi,77,55,19.25,83,low
350916,Jenggawah,235,224,58.75,47,high
350917,Ajung,88,77,22,80,low
350918,Tempurejo,74,48,18.5,84,low
350919,Kaliwates,194,139,48.5,56,medium
350920,Patrang,202,145,50.5,54,medium
350921,Sumbersari,217,138,54.25,51,medium
350922,Arjasa,116,81,29,74,low
350923,Mumbulsari,99,81,24.75,78,low
350924,Pakusari,152,129,38,66,low
350925,Jelbuk,132,90,33,70,low
350926,Mayang,89,60,22.25,80,low
350927,Kalisat,270,163,67.5,39,high
350928,Ledokombo,103,76,25.75,77,low
350929,Sukowono,171,125,42.75,61,low
350930,Silo,143,85,35.75,68,high
350931,Sumberjambe,109,94,27.25,75,low
1 district_id district_name crime_total crime_cleared avg_crime score level
2 350901 Jombang 118 110 29.5 73 low
3 350902 Kencong 91 74 22.75 80 low
4 350903 Sumberbaru 157 130 39.25 64 high
5 350904 Gumukmas 91 78 22.75 80 low
6 350905 Umbulsari 115 88 28.75 74 low
7 350906 Tanggul 266 213 66.5 39 high
8 350907 Semboro 94 89 23.5 79 low
9 350908 Puger 180 160 45 59 high
10 350909 Bangsalsari 154 132 38.5 65 high
11 350910 Balung 278 223 69.5 37 high
12 350911 Wuluhan 216 176 54 51 high
13 350912 Ambulu 157 124 39.25 64 high
14 350913 Rambipuji 278 170 69.5 37 low
15 350914 Panti 139 109 34.75 69 low
16 350915 Sukorambi 77 55 19.25 83 low
17 350916 Jenggawah 235 224 58.75 47 high
18 350917 Ajung 88 77 22 80 low
19 350918 Tempurejo 74 48 18.5 84 low
20 350919 Kaliwates 194 139 48.5 56 medium
21 350920 Patrang 202 145 50.5 54 medium
22 350921 Sumbersari 217 138 54.25 51 medium
23 350922 Arjasa 116 81 29 74 low
24 350923 Mumbulsari 99 81 24.75 78 low
25 350924 Pakusari 152 129 38 66 low
26 350925 Jelbuk 132 90 33 70 low
27 350926 Mayang 89 60 22.25 80 low
28 350927 Kalisat 270 163 67.5 39 high
29 350928 Ledokombo 103 76 25.75 77 low
30 350929 Sukowono 171 125 42.75 61 low
31 350930 Silo 143 85 35.75 68 high
32 350931 Sumberjambe 109 94 27.25 75 low

View File

@ -1,156 +1,187 @@
district_id,district_name,number_of_crime,level,score,method,year
350901,Jombang,10,low,93,kmeans,2020
350901,Jombang,19,low,87,kmeans,2021
350901,Jombang,4,low,98,kmeans,2022
350901,Jombang,4,low,98,kmeans,2023
350901,Jombang,4,low,98,kmeans,2024
350902,Kencong,6,low,96,kmeans,2022
350902,Kencong,10,low,93,kmeans,2021
350902,Kencong,11,low,93,kmeans,2020
350902,Kencong,7,low,95,kmeans,2023
350902,Kencong,4,low,98,kmeans,2024
350903,Sumberbaru,82,medium,42,kmeans,2024
350903,Sumberbaru,75,medium,47,kmeans,2022
350903,Sumberbaru,109,medium,23,kmeans,2021
350903,Sumberbaru,49,medium,65,kmeans,2023
350903,Sumberbaru,85,medium,40,kmeans,2020
350904,Gumukmas,101,medium,28,kmeans,2020
350904,Gumukmas,73,medium,48,kmeans,2022
350904,Gumukmas,104,medium,26,kmeans,2021
350904,Gumukmas,74,low,48,kmeans,2024
350904,Gumukmas,49,low,65,kmeans,2023
350905,Umbulsari,7,low,95,kmeans,2022
350905,Umbulsari,5,low,97,kmeans,2023
350905,Umbulsari,17,low,88,kmeans,2021
350905,Umbulsari,14,low,90,kmeans,2020
350905,Umbulsari,2,low,99,kmeans,2024
350906,Tanggul,102,medium,28,kmeans,2020
350906,Tanggul,69,medium,51,kmeans,2023
350906,Tanggul,95,medium,33,kmeans,2021
350906,Tanggul,29,low,80,kmeans,2024
350906,Tanggul,93,medium,34,kmeans,2022
350907,Semboro,6,low,96,kmeans,2022
350907,Semboro,5,low,97,kmeans,2023
350907,Semboro,21,low,85,kmeans,2021
350907,Semboro,4,low,98,kmeans,2024
350907,Semboro,13,low,91,kmeans,2020
350908,Puger,102,medium,28,kmeans,2020
350908,Puger,72,medium,49,kmeans,2024
350908,Puger,98,medium,30,kmeans,2021
350908,Puger,94,medium,33,kmeans,2023
350908,Puger,82,medium,42,kmeans,2022
350909,Bangsalsari,116,medium,18,kmeans,2022
350909,Bangsalsari,75,medium,47,kmeans,2023
350909,Bangsalsari,121,medium,14,kmeans,2021
350909,Bangsalsari,64,medium,55,kmeans,2024
350909,Bangsalsari,97,medium,31,kmeans,2020
350910,Balung,122,medium,13,kmeans,2020
350910,Balung,92,medium,35,kmeans,2023
350910,Balung,127,medium,10,kmeans,2021
350910,Balung,102,medium,28,kmeans,2024
350910,Balung,81,medium,43,kmeans,2022
350911,Wuluhan,72,medium,49,kmeans,2022
350911,Wuluhan,74,medium,48,kmeans,2024
350911,Wuluhan,132,medium,6,kmeans,2021
350911,Wuluhan,84,medium,40,kmeans,2023
350911,Wuluhan,96,medium,32,kmeans,2020
350912,Ambulu,99,medium,30,kmeans,2020
350912,Ambulu,70,medium,50,kmeans,2024
350912,Ambulu,97,medium,31,kmeans,2021
350912,Ambulu,99,medium,30,kmeans,2023
350912,Ambulu,77,medium,45,kmeans,2022
350913,Rambipuji,104,medium,26,kmeans,2022
350913,Rambipuji,68,medium,52,kmeans,2023
350913,Rambipuji,103,medium,27,kmeans,2021
350913,Rambipuji,103,medium,27,kmeans,2020
350913,Rambipuji,48,medium,66,kmeans,2024
350914,Panti,11,low,93,kmeans,2020
350914,Panti,5,low,97,kmeans,2023
350914,Panti,19,low,87,kmeans,2021
350914,Panti,3,low,98,kmeans,2024
350914,Panti,7,low,95,kmeans,2022
350915,Sukorambi,4,low,98,kmeans,2022
350915,Sukorambi,5,low,97,kmeans,2024
350915,Sukorambi,19,low,87,kmeans,2021
350915,Sukorambi,6,low,96,kmeans,2023
350915,Sukorambi,11,low,93,kmeans,2020
350916,Jenggawah,59,medium,58,kmeans,2023
350916,Jenggawah,66,medium,53,kmeans,2024
350916,Jenggawah,96,medium,32,kmeans,2022
350916,Jenggawah,106,medium,25,kmeans,2020
350916,Jenggawah,111,medium,21,kmeans,2021
350917,Ajung,107,medium,24,kmeans,2021
350917,Ajung,82,medium,42,kmeans,2020
350917,Ajung,95,medium,33,kmeans,2022
350917,Ajung,82,medium,42,kmeans,2024
350917,Ajung,63,medium,55,kmeans,2023
350918,Tempurejo,15,low,90,kmeans,2023
350918,Tempurejo,17,low,88,kmeans,2024
350918,Tempurejo,27,low,81,kmeans,2022
350918,Tempurejo,39,low,73,kmeans,2020
350918,Tempurejo,49,low,65,kmeans,2021
350919,Kaliwates,124,high,12,kmeans,2021
350919,Kaliwates,100,high,29,kmeans,2024
350919,Kaliwates,93,high,34,kmeans,2022
350919,Kaliwates,89,high,37,kmeans,2020
350919,Kaliwates,70,high,50,kmeans,2023
350920,Patrang,52,high,63,kmeans,2023
350920,Patrang,104,high,26,kmeans,2024
350920,Patrang,88,high,38,kmeans,2022
350920,Patrang,124,high,12,kmeans,2020
350920,Patrang,140,high,0,kmeans,2021
350921,Sumbersari,89,high,37,kmeans,2021
350921,Sumbersari,94,medium,33,kmeans,2020
350921,Sumbersari,107,high,24,kmeans,2022
350921,Sumbersari,53,high,63,kmeans,2023
350921,Sumbersari,79,high,44,kmeans,2024
350922,Arjasa,6,low,96,kmeans,2023
350922,Arjasa,8,low,95,kmeans,2020
350922,Arjasa,14,low,90,kmeans,2022
350922,Arjasa,3,low,98,kmeans,2024
350922,Arjasa,26,low,82,kmeans,2021
350923,Mumbulsari,17,low,88,kmeans,2021
350923,Mumbulsari,4,low,98,kmeans,2024
350923,Mumbulsari,2,low,99,kmeans,2022
350923,Mumbulsari,10,low,93,kmeans,2020
350923,Mumbulsari,2,low,99,kmeans,2023
350924,Pakusari,7,low,95,kmeans,2023
350924,Pakusari,3,low,98,kmeans,2024
350924,Pakusari,10,low,93,kmeans,2022
350924,Pakusari,11,low,93,kmeans,2020
350924,Pakusari,21,low,85,kmeans,2021
350925,Jelbuk,21,low,85,kmeans,2021
350925,Jelbuk,12,low,92,kmeans,2020
350925,Jelbuk,6,low,96,kmeans,2022
350925,Jelbuk,0,low,100,kmeans,2024
350925,Jelbuk,3,low,98,kmeans,2023
350926,Mayang,7,low,95,kmeans,2023
350926,Mayang,16,low,89,kmeans,2020
350926,Mayang,6,low,96,kmeans,2022
350926,Mayang,18,low,88,kmeans,2021
350926,Mayang,4,low,98,kmeans,2024
350927,Kalisat,16,low,89,kmeans,2020
350927,Kalisat,15,low,90,kmeans,2021
350927,Kalisat,4,low,98,kmeans,2023
350927,Kalisat,5,low,97,kmeans,2022
350927,Kalisat,5,low,97,kmeans,2024
350928,Ledokombo,5,low,97,kmeans,2024
350928,Ledokombo,18,low,88,kmeans,2021
350928,Ledokombo,10,low,93,kmeans,2020
350928,Ledokombo,10,low,93,kmeans,2022
350928,Ledokombo,4,low,98,kmeans,2023
350929,Sukowono,6,low,96,kmeans,2023
350929,Sukowono,7,low,95,kmeans,2022
350929,Sukowono,11,low,93,kmeans,2020
350929,Sukowono,20,low,86,kmeans,2021
350929,Sukowono,2,low,99,kmeans,2024
350930,Silo,68,medium,52,kmeans,2024
350930,Silo,86,medium,39,kmeans,2022
350930,Silo,81,medium,43,kmeans,2023
350930,Silo,89,medium,37,kmeans,2021
350930,Silo,109,medium,23,kmeans,2020
350931,Sumberjambe,7,low,95,kmeans,2020
350931,Sumberjambe,19,low,87,kmeans,2021
350931,Sumberjambe,8,low,95,kmeans,2023
350931,Sumberjambe,5,low,97,kmeans,2022
350931,Sumberjambe,4,low,98,kmeans,2024
district_id,district_name,number_of_crime,avg_crime,crime_cleared,level,score,method,year
350901,Jombang,10,0.83,10,low,93,kmeans,2020
350901,Jombang,19,1.58,19,low,87,kmeans,2021
350901,Jombang,4,0.33,4,low,98,kmeans,2022
350901,Jombang,4,0.33,4,low,98,kmeans,2023
350901,Jombang,4,0.33,4,low,98,kmeans,2024
350901,Jombang,71,5.92,40,medium,50,kmeans,2025
350902,Kencong,11,0.92,9,low,93,kmeans,2020
350902,Kencong,10,0.83,10,low,93,kmeans,2021
350902,Kencong,6,0.5,6,low,96,kmeans,2022
350902,Kencong,7,0.58,7,low,95,kmeans,2023
350902,Kencong,4,0.33,5,low,98,kmeans,2024
350902,Kencong,57,4.75,7,medium,60,kmeans,2025
350903,Sumberbaru,85,7.08,73,medium,40,kmeans,2020
350903,Sumberbaru,109,9.08,87,medium,23,kmeans,2021
350903,Sumberbaru,75,6.25,63,medium,47,kmeans,2022
350903,Sumberbaru,49,4.08,39,medium,65,kmeans,2023
350903,Sumberbaru,82,6.83,70,medium,42,kmeans,2024
350903,Sumberbaru,58,4.83,38,medium,59,kmeans,2025
350904,Gumukmas,101,8.42,80,medium,28,kmeans,2020
350904,Gumukmas,104,8.67,88,medium,26,kmeans,2021
350904,Gumukmas,73,6.08,69,medium,48,kmeans,2022
350904,Gumukmas,49,4.08,40,medium,65,kmeans,2023
350904,Gumukmas,74,6.17,60,low,48,kmeans,2024
350904,Gumukmas,55,4.58,32,medium,61,kmeans,2025
350905,Umbulsari,14,1.17,13,low,90,kmeans,2020
350905,Umbulsari,17,1.42,17,low,88,kmeans,2021
350905,Umbulsari,7,0.58,7,low,95,kmeans,2022
350905,Umbulsari,5,0.42,5,low,97,kmeans,2023
350905,Umbulsari,2,0.17,2,low,99,kmeans,2024
350905,Umbulsari,79,6.58,33,medium,44,kmeans,2025
350906,Tanggul,102,8.5,78,medium,28,kmeans,2020
350906,Tanggul,95,7.92,86,medium,33,kmeans,2021
350906,Tanggul,93,7.75,83,medium,34,kmeans,2022
350906,Tanggul,69,5.75,63,medium,51,kmeans,2023
350906,Tanggul,29,2.42,27,low,80,kmeans,2024
350906,Tanggul,113,9.42,20,medium,20,kmeans,2025
350907,Semboro,13,1.08,11,low,91,kmeans,2020
350907,Semboro,21,1.75,21,low,85,kmeans,2021
350907,Semboro,6,0.5,6,low,96,kmeans,2022
350907,Semboro,5,0.42,5,low,97,kmeans,2023
350907,Semboro,4,0.33,4,low,98,kmeans,2024
350907,Semboro,78,6.5,32,medium,45,kmeans,2025
350908,Puger,102,8.5,85,medium,28,kmeans,2020
350908,Puger,98,8.17,84,medium,30,kmeans,2021
350908,Puger,82,6.83,74,medium,42,kmeans,2022
350908,Puger,94,7.83,76,medium,33,kmeans,2023
350908,Puger,72,6,66,medium,49,kmeans,2024
350908,Puger,105,8.75,64,medium,25,kmeans,2025
350909,Bangsalsari,97,8.08,80,medium,31,kmeans,2020
350909,Bangsalsari,121,10.08,103,medium,14,kmeans,2021
350909,Bangsalsari,116,9.67,102,medium,18,kmeans,2022
350909,Bangsalsari,75,6.25,65,medium,47,kmeans,2023
350909,Bangsalsari,64,5.33,53,medium,55,kmeans,2024
350909,Bangsalsari,95,7.92,55,medium,33,kmeans,2025
350910,Balung,122,10.17,94,medium,13,kmeans,2020
350910,Balung,127,10.58,104,medium,10,kmeans,2021
350910,Balung,81,6.75,71,medium,43,kmeans,2022
350910,Balung,92,7.67,74,medium,35,kmeans,2023
350910,Balung,102,8.5,79,medium,28,kmeans,2024
350910,Balung,58,4.83,16,medium,59,kmeans,2025
350911,Wuluhan,96,8,77,medium,32,kmeans,2020
350911,Wuluhan,132,11,103,medium,6,kmeans,2021
350911,Wuluhan,72,6,67,medium,49,kmeans,2022
350911,Wuluhan,84,7,70,medium,40,kmeans,2023
350911,Wuluhan,74,6.17,62,medium,48,kmeans,2024
350911,Wuluhan,62,5.17,31,medium,56,kmeans,2025
350912,Ambulu,99,8.25,79,medium,30,kmeans,2020
350912,Ambulu,97,8.08,83,medium,31,kmeans,2021
350912,Ambulu,77,6.42,73,medium,45,kmeans,2022
350912,Ambulu,99,8.25,75,medium,30,kmeans,2023
350912,Ambulu,70,5.83,56,medium,50,kmeans,2024
350912,Ambulu,92,7.67,55,medium,35,kmeans,2025
350913,Rambipuji,103,8.58,86,medium,27,kmeans,2020
350913,Rambipuji,103,8.58,91,medium,27,kmeans,2021
350913,Rambipuji,104,8.67,96,medium,26,kmeans,2022
350913,Rambipuji,68,5.67,58,medium,52,kmeans,2023
350913,Rambipuji,48,4,42,medium,66,kmeans,2024
350913,Rambipuji,74,6.17,47,medium,48,kmeans,2025
350914,Panti,11,0.92,9,low,93,kmeans,2020
350914,Panti,19,1.58,19,low,87,kmeans,2021
350914,Panti,7,0.58,7,low,95,kmeans,2022
350914,Panti,5,0.42,5,low,97,kmeans,2023
350914,Panti,3,0.25,3,low,98,kmeans,2024
350914,Panti,90,7.5,54,medium,36,kmeans,2025
350915,Sukorambi,11,0.92,10,low,93,kmeans,2020
350915,Sukorambi,19,1.58,19,low,87,kmeans,2021
350915,Sukorambi,4,0.33,4,low,98,kmeans,2022
350915,Sukorambi,6,0.5,6,low,96,kmeans,2023
350915,Sukorambi,5,0.42,5,low,97,kmeans,2024
350915,Sukorambi,89,7.42,21,medium,37,kmeans,2025
350916,Jenggawah,106,8.83,83,medium,25,kmeans,2020
350916,Jenggawah,111,9.25,92,medium,21,kmeans,2021
350916,Jenggawah,96,8,80,medium,32,kmeans,2022
350916,Jenggawah,59,4.92,54,medium,58,kmeans,2023
350916,Jenggawah,66,5.5,53,medium,53,kmeans,2024
350916,Jenggawah,77,6.42,27,medium,45,kmeans,2025
350917,Ajung,82,6.83,71,medium,42,kmeans,2020
350917,Ajung,107,8.92,92,medium,24,kmeans,2021
350917,Ajung,95,7.92,87,medium,33,kmeans,2022
350917,Ajung,63,5.25,56,medium,55,kmeans,2023
350917,Ajung,82,6.83,57,medium,42,kmeans,2024
350917,Ajung,87,7.25,66,medium,38,kmeans,2025
350918,Tempurejo,39,3.25,35,low,73,kmeans,2020
350918,Tempurejo,49,4.08,48,low,65,kmeans,2021
350918,Tempurejo,27,2.25,27,low,81,kmeans,2022
350918,Tempurejo,15,1.25,16,low,90,kmeans,2023
350918,Tempurejo,17,1.42,17,low,88,kmeans,2024
350918,Tempurejo,61,5.08,26,medium,57,kmeans,2025
350919,Kaliwates,89,7.42,73,high,37,kmeans,2020
350919,Kaliwates,124,10.33,105,high,12,kmeans,2021
350919,Kaliwates,93,7.75,80,high,34,kmeans,2022
350919,Kaliwates,70,5.83,60,high,50,kmeans,2023
350919,Kaliwates,100,8.33,82,high,29,kmeans,2024
350919,Kaliwates,71,5.92,46,medium,50,kmeans,2025
350920,Patrang,124,10.33,98,high,12,kmeans,2020
350920,Patrang,140,11.67,110,high,0,kmeans,2021
350920,Patrang,88,7.33,83,high,38,kmeans,2022
350920,Patrang,52,4.33,48,high,63,kmeans,2023
350920,Patrang,104,8.67,77,high,26,kmeans,2024
350920,Patrang,66,5.5,27,medium,53,kmeans,2025
350921,Sumbersari,94,7.83,77,medium,33,kmeans,2020
350921,Sumbersari,89,7.42,79,high,37,kmeans,2021
350921,Sumbersari,107,8.92,89,high,24,kmeans,2022
350921,Sumbersari,53,4.42,51,high,63,kmeans,2023
350921,Sumbersari,79,6.58,63,high,44,kmeans,2024
350921,Sumbersari,107,8.92,42,medium,24,kmeans,2025
350922,Arjasa,8,0.67,6,low,95,kmeans,2020
350922,Arjasa,26,2.17,26,low,82,kmeans,2021
350922,Arjasa,14,1.17,14,low,90,kmeans,2022
350922,Arjasa,6,0.5,6,low,96,kmeans,2023
350922,Arjasa,3,0.25,4,low,98,kmeans,2024
350922,Arjasa,99,8.25,56,medium,30,kmeans,2025
350923,Mumbulsari,10,0.83,9,low,93,kmeans,2020
350923,Mumbulsari,17,1.42,18,low,88,kmeans,2021
350923,Mumbulsari,2,0.17,2,low,99,kmeans,2022
350923,Mumbulsari,2,0.17,2,low,99,kmeans,2023
350923,Mumbulsari,4,0.33,4,low,98,kmeans,2024
350923,Mumbulsari,68,5.67,35,medium,52,kmeans,2025
350924,Pakusari,11,0.92,12,low,93,kmeans,2020
350924,Pakusari,21,1.75,21,low,85,kmeans,2021
350924,Pakusari,10,0.83,10,low,93,kmeans,2022
350924,Pakusari,7,0.58,7,low,95,kmeans,2023
350924,Pakusari,3,0.25,3,low,98,kmeans,2024
350924,Pakusari,61,5.08,43,medium,57,kmeans,2025
350925,Jelbuk,12,1,11,low,92,kmeans,2020
350925,Jelbuk,21,1.75,21,low,85,kmeans,2021
350925,Jelbuk,6,0.5,6,low,96,kmeans,2022
350925,Jelbuk,3,0.25,3,low,98,kmeans,2023
350925,Jelbuk,0,0,0,low,100,kmeans,2024
350925,Jelbuk,102,8.5,76,medium,28,kmeans,2025
350926,Mayang,16,1.33,15,low,89,kmeans,2020
350926,Mayang,18,1.5,19,low,88,kmeans,2021
350926,Mayang,6,0.5,6,low,96,kmeans,2022
350926,Mayang,7,0.58,7,low,95,kmeans,2023
350926,Mayang,4,0.33,4,low,98,kmeans,2024
350926,Mayang,90,7.5,46,medium,36,kmeans,2025
350927,Kalisat,16,1.33,15,low,89,kmeans,2020
350927,Kalisat,15,1.25,16,low,90,kmeans,2021
350927,Kalisat,5,0.42,5,low,97,kmeans,2022
350927,Kalisat,4,0.33,4,low,98,kmeans,2023
350927,Kalisat,5,0.42,5,low,97,kmeans,2024
350927,Kalisat,67,5.58,46,medium,53,kmeans,2025
350928,Ledokombo,10,0.83,8,low,93,kmeans,2020
350928,Ledokombo,18,1.5,18,low,88,kmeans,2021
350928,Ledokombo,10,0.83,10,low,93,kmeans,2022
350928,Ledokombo,4,0.33,4,low,98,kmeans,2023
350928,Ledokombo,5,0.42,5,low,97,kmeans,2024
350928,Ledokombo,71,5.92,46,medium,50,kmeans,2025
350929,Sukowono,11,0.92,11,low,93,kmeans,2020
350929,Sukowono,20,1.67,20,low,86,kmeans,2021
350929,Sukowono,7,0.58,7,low,95,kmeans,2022
350929,Sukowono,6,0.5,6,low,96,kmeans,2023
350929,Sukowono,2,0.17,3,low,99,kmeans,2024
350929,Sukowono,66,5.5,30,medium,53,kmeans,2025
350930,Silo,109,9.08,92,medium,23,kmeans,2020
350930,Silo,89,7.42,79,medium,37,kmeans,2021
350930,Silo,86,7.17,80,medium,39,kmeans,2022
350930,Silo,81,6.75,68,medium,43,kmeans,2023
350930,Silo,68,5.67,62,medium,52,kmeans,2024
350930,Silo,89,7.42,27,medium,37,kmeans,2025
350931,Sumberjambe,7,0.58,8,low,95,kmeans,2020
350931,Sumberjambe,19,1.58,19,low,87,kmeans,2021
350931,Sumberjambe,5,0.42,5,low,97,kmeans,2022
350931,Sumberjambe,8,0.67,8,low,95,kmeans,2023
350931,Sumberjambe,4,0.33,4,low,98,kmeans,2024
350931,Sumberjambe,98,8.17,56,medium,30,kmeans,2025

1 district_id district_name number_of_crime avg_crime crime_cleared level score method year
2 350901 Jombang 10 0.83 10 low 93 kmeans 2020
3 350901 Jombang 19 1.58 19 low 87 kmeans 2021
4 350901 Jombang 4 0.33 4 low 98 kmeans 2022
5 350901 Jombang 4 0.33 4 low 98 kmeans 2023
6 350901 Jombang 4 0.33 4 low 98 kmeans 2024
7 350902 350901 Kencong Jombang 6 71 5.92 40 low medium 96 50 kmeans 2022 2025
8 350902 Kencong 10 11 0.92 9 low 93 kmeans 2021 2020
9 350902 Kencong 11 10 0.83 10 low 93 kmeans 2020 2021
10 350902 Kencong 7 6 0.5 6 low 95 96 kmeans 2023 2022
11 350902 Kencong 4 7 0.58 7 low 98 95 kmeans 2024 2023
12 350903 350902 Sumberbaru Kencong 82 4 0.33 5 medium low 42 98 kmeans 2024
13 350903 350902 Sumberbaru Kencong 75 57 4.75 7 medium 47 60 kmeans 2022 2025
14 350903 Sumberbaru 109 85 7.08 73 medium 23 40 kmeans 2021 2020
15 350903 Sumberbaru 49 109 9.08 87 medium 65 23 kmeans 2023 2021
16 350903 Sumberbaru 85 75 6.25 63 medium 40 47 kmeans 2020 2022
17 350904 350903 Gumukmas Sumberbaru 101 49 4.08 39 medium 28 65 kmeans 2020 2023
18 350904 350903 Gumukmas Sumberbaru 73 82 6.83 70 medium 48 42 kmeans 2022 2024
19 350904 350903 Gumukmas Sumberbaru 104 58 4.83 38 medium 26 59 kmeans 2021 2025
20 350904 Gumukmas 74 101 8.42 80 low medium 48 28 kmeans 2024 2020
21 350904 Gumukmas 49 104 8.67 88 low medium 65 26 kmeans 2023 2021
22 350905 350904 Umbulsari Gumukmas 7 73 6.08 69 low medium 95 48 kmeans 2022
23 350905 350904 Umbulsari Gumukmas 5 49 4.08 40 low medium 97 65 kmeans 2023
24 350905 350904 Umbulsari Gumukmas 17 74 6.17 60 low 88 48 kmeans 2021 2024
25 350905 350904 Umbulsari Gumukmas 14 55 4.58 32 low medium 90 61 kmeans 2020 2025
26 350905 Umbulsari 2 14 1.17 13 low 99 90 kmeans 2024 2020
27 350906 350905 Tanggul Umbulsari 102 17 1.42 17 medium low 28 88 kmeans 2020 2021
28 350906 350905 Tanggul Umbulsari 69 7 0.58 7 medium low 51 95 kmeans 2023 2022
29 350906 350905 Tanggul Umbulsari 95 5 0.42 5 medium low 33 97 kmeans 2021 2023
30 350906 350905 Tanggul Umbulsari 29 2 0.17 2 low 80 99 kmeans 2024
31 350906 350905 Tanggul Umbulsari 93 79 6.58 33 medium 34 44 kmeans 2022 2025
32 350907 350906 Semboro Tanggul 6 102 8.5 78 low medium 96 28 kmeans 2022 2020
33 350907 350906 Semboro Tanggul 5 95 7.92 86 low medium 97 33 kmeans 2023 2021
34 350907 350906 Semboro Tanggul 21 93 7.75 83 low medium 85 34 kmeans 2021 2022
35 350907 350906 Semboro Tanggul 4 69 5.75 63 low medium 98 51 kmeans 2024 2023
36 350907 350906 Semboro Tanggul 13 29 2.42 27 low 91 80 kmeans 2020 2024
37 350908 350906 Puger Tanggul 102 113 9.42 20 medium 28 20 kmeans 2020 2025
38 350908 350907 Puger Semboro 72 13 1.08 11 medium low 49 91 kmeans 2024 2020
39 350908 350907 Puger Semboro 98 21 1.75 21 medium low 30 85 kmeans 2021
40 350908 350907 Puger Semboro 94 6 0.5 6 medium low 33 96 kmeans 2023 2022
41 350908 350907 Puger Semboro 82 5 0.42 5 medium low 42 97 kmeans 2022 2023
42 350909 350907 Bangsalsari Semboro 116 4 0.33 4 medium low 18 98 kmeans 2022 2024
43 350909 350907 Bangsalsari Semboro 75 78 6.5 32 medium 47 45 kmeans 2023 2025
44 350909 350908 Bangsalsari Puger 121 102 8.5 85 medium 14 28 kmeans 2021 2020
45 350909 350908 Bangsalsari Puger 64 98 8.17 84 medium 55 30 kmeans 2024 2021
46 350909 350908 Bangsalsari Puger 97 82 6.83 74 medium 31 42 kmeans 2020 2022
47 350910 350908 Balung Puger 122 94 7.83 76 medium 13 33 kmeans 2020 2023
48 350910 350908 Balung Puger 92 72 6 66 medium 35 49 kmeans 2023 2024
49 350910 350908 Balung Puger 127 105 8.75 64 medium 10 25 kmeans 2021 2025
50 350910 350909 Balung Bangsalsari 102 97 8.08 80 medium 28 31 kmeans 2024 2020
51 350910 350909 Balung Bangsalsari 81 121 10.08 103 medium 43 14 kmeans 2022 2021
52 350911 350909 Wuluhan Bangsalsari 72 116 9.67 102 medium 49 18 kmeans 2022
53 350911 350909 Wuluhan Bangsalsari 74 75 6.25 65 medium 48 47 kmeans 2024 2023
54 350911 350909 Wuluhan Bangsalsari 132 64 5.33 53 medium 6 55 kmeans 2021 2024
55 350911 350909 Wuluhan Bangsalsari 84 95 7.92 55 medium 40 33 kmeans 2023 2025
56 350911 350910 Wuluhan Balung 96 122 10.17 94 medium 32 13 kmeans 2020
57 350912 350910 Ambulu Balung 99 127 10.58 104 medium 30 10 kmeans 2020 2021
58 350912 350910 Ambulu Balung 70 81 6.75 71 medium 50 43 kmeans 2024 2022
59 350912 350910 Ambulu Balung 97 92 7.67 74 medium 31 35 kmeans 2021 2023
60 350912 350910 Ambulu Balung 99 102 8.5 79 medium 30 28 kmeans 2023 2024
61 350912 350910 Ambulu Balung 77 58 4.83 16 medium 45 59 kmeans 2022 2025
62 350913 350911 Rambipuji Wuluhan 104 96 8 77 medium 26 32 kmeans 2022 2020
63 350913 350911 Rambipuji Wuluhan 68 132 11 103 medium 52 6 kmeans 2023 2021
64 350913 350911 Rambipuji Wuluhan 103 72 6 67 medium 27 49 kmeans 2021 2022
65 350913 350911 Rambipuji Wuluhan 103 84 7 70 medium 27 40 kmeans 2020 2023
66 350913 350911 Rambipuji Wuluhan 48 74 6.17 62 medium 66 48 kmeans 2024
67 350914 350911 Panti Wuluhan 11 62 5.17 31 low medium 93 56 kmeans 2020 2025
68 350914 350912 Panti Ambulu 5 99 8.25 79 low medium 97 30 kmeans 2023 2020
69 350914 350912 Panti Ambulu 19 97 8.08 83 low medium 87 31 kmeans 2021
70 350914 350912 Panti Ambulu 3 77 6.42 73 low medium 98 45 kmeans 2024 2022
71 350914 350912 Panti Ambulu 7 99 8.25 75 low medium 95 30 kmeans 2022 2023
72 350915 350912 Sukorambi Ambulu 4 70 5.83 56 low medium 98 50 kmeans 2022 2024
73 350915 350912 Sukorambi Ambulu 5 92 7.67 55 low medium 97 35 kmeans 2024 2025
74 350915 350913 Sukorambi Rambipuji 19 103 8.58 86 low medium 87 27 kmeans 2021 2020
75 350915 350913 Sukorambi Rambipuji 6 103 8.58 91 low medium 96 27 kmeans 2023 2021
76 350915 350913 Sukorambi Rambipuji 11 104 8.67 96 low medium 93 26 kmeans 2020 2022
77 350916 350913 Jenggawah Rambipuji 59 68 5.67 58 medium 58 52 kmeans 2023
78 350916 350913 Jenggawah Rambipuji 66 48 4 42 medium 53 66 kmeans 2024
79 350916 350913 Jenggawah Rambipuji 96 74 6.17 47 medium 32 48 kmeans 2022 2025
80 350916 350914 Jenggawah Panti 106 11 0.92 9 medium low 25 93 kmeans 2020
81 350916 350914 Jenggawah Panti 111 19 1.58 19 medium low 21 87 kmeans 2021
82 350917 350914 Ajung Panti 107 7 0.58 7 medium low 24 95 kmeans 2021 2022
83 350917 350914 Ajung Panti 82 5 0.42 5 medium low 42 97 kmeans 2020 2023
84 350917 350914 Ajung Panti 95 3 0.25 3 medium low 33 98 kmeans 2022 2024
85 350917 350914 Ajung Panti 82 90 7.5 54 medium 42 36 kmeans 2024 2025
86 350917 350915 Ajung Sukorambi 63 11 0.92 10 medium low 55 93 kmeans 2023 2020
87 350918 350915 Tempurejo Sukorambi 15 19 1.58 19 low 90 87 kmeans 2023 2021
88 350918 350915 Tempurejo Sukorambi 17 4 0.33 4 low 88 98 kmeans 2024 2022
89 350918 350915 Tempurejo Sukorambi 27 6 0.5 6 low 81 96 kmeans 2022 2023
90 350918 350915 Tempurejo Sukorambi 39 5 0.42 5 low 73 97 kmeans 2020 2024
91 350918 350915 Tempurejo Sukorambi 49 89 7.42 21 low medium 65 37 kmeans 2021 2025
92 350919 350916 Kaliwates Jenggawah 124 106 8.83 83 high medium 12 25 kmeans 2021 2020
93 350919 350916 Kaliwates Jenggawah 100 111 9.25 92 high medium 29 21 kmeans 2024 2021
94 350919 350916 Kaliwates Jenggawah 93 96 8 80 high medium 34 32 kmeans 2022
95 350919 350916 Kaliwates Jenggawah 89 59 4.92 54 high medium 37 58 kmeans 2020 2023
96 350919 350916 Kaliwates Jenggawah 70 66 5.5 53 high medium 50 53 kmeans 2023 2024
97 350920 350916 Patrang Jenggawah 52 77 6.42 27 high medium 63 45 kmeans 2023 2025
98 350920 350917 Patrang Ajung 104 82 6.83 71 high medium 26 42 kmeans 2024 2020
99 350920 350917 Patrang Ajung 88 107 8.92 92 high medium 38 24 kmeans 2022 2021
100 350920 350917 Patrang Ajung 124 95 7.92 87 high medium 12 33 kmeans 2020 2022
101 350920 350917 Patrang Ajung 140 63 5.25 56 high medium 0 55 kmeans 2021 2023
102 350921 350917 Sumbersari Ajung 89 82 6.83 57 high medium 37 42 kmeans 2021 2024
103 350921 350917 Sumbersari Ajung 94 87 7.25 66 medium 33 38 kmeans 2020 2025
104 350921 350918 Sumbersari Tempurejo 107 39 3.25 35 high low 24 73 kmeans 2022 2020
105 350921 350918 Sumbersari Tempurejo 53 49 4.08 48 high low 63 65 kmeans 2023 2021
106 350921 350918 Sumbersari Tempurejo 79 27 2.25 27 high low 44 81 kmeans 2024 2022
107 350922 350918 Arjasa Tempurejo 6 15 1.25 16 low 96 90 kmeans 2023
108 350922 350918 Arjasa Tempurejo 8 17 1.42 17 low 95 88 kmeans 2020 2024
109 350922 350918 Arjasa Tempurejo 14 61 5.08 26 low medium 90 57 kmeans 2022 2025
110 350922 350919 Arjasa Kaliwates 3 89 7.42 73 low high 98 37 kmeans 2024 2020
111 350922 350919 Arjasa Kaliwates 26 124 10.33 105 low high 82 12 kmeans 2021
112 350923 350919 Mumbulsari Kaliwates 17 93 7.75 80 low high 88 34 kmeans 2021 2022
113 350923 350919 Mumbulsari Kaliwates 4 70 5.83 60 low high 98 50 kmeans 2024 2023
114 350923 350919 Mumbulsari Kaliwates 2 100 8.33 82 low high 99 29 kmeans 2022 2024
115 350923 350919 Mumbulsari Kaliwates 10 71 5.92 46 low medium 93 50 kmeans 2020 2025
116 350923 350920 Mumbulsari Patrang 2 124 10.33 98 low high 99 12 kmeans 2023 2020
117 350924 350920 Pakusari Patrang 7 140 11.67 110 low high 95 0 kmeans 2023 2021
118 350924 350920 Pakusari Patrang 3 88 7.33 83 low high 98 38 kmeans 2024 2022
119 350924 350920 Pakusari Patrang 10 52 4.33 48 low high 93 63 kmeans 2022 2023
120 350924 350920 Pakusari Patrang 11 104 8.67 77 low high 93 26 kmeans 2020 2024
121 350924 350920 Pakusari Patrang 21 66 5.5 27 low medium 85 53 kmeans 2021 2025
122 350925 350921 Jelbuk Sumbersari 21 94 7.83 77 low medium 85 33 kmeans 2021 2020
123 350925 350921 Jelbuk Sumbersari 12 89 7.42 79 low high 92 37 kmeans 2020 2021
124 350925 350921 Jelbuk Sumbersari 6 107 8.92 89 low high 96 24 kmeans 2022
125 350925 350921 Jelbuk Sumbersari 0 53 4.42 51 low high 100 63 kmeans 2024 2023
126 350925 350921 Jelbuk Sumbersari 3 79 6.58 63 low high 98 44 kmeans 2023 2024
127 350926 350921 Mayang Sumbersari 7 107 8.92 42 low medium 95 24 kmeans 2023 2025
128 350926 350922 Mayang Arjasa 16 8 0.67 6 low 89 95 kmeans 2020
129 350926 350922 Mayang Arjasa 6 26 2.17 26 low 96 82 kmeans 2022 2021
130 350926 350922 Mayang Arjasa 18 14 1.17 14 low 88 90 kmeans 2021 2022
131 350926 350922 Mayang Arjasa 4 6 0.5 6 low 98 96 kmeans 2024 2023
132 350927 350922 Kalisat Arjasa 16 3 0.25 4 low 89 98 kmeans 2020 2024
133 350927 350922 Kalisat Arjasa 15 99 8.25 56 low medium 90 30 kmeans 2021 2025
134 350927 350923 Kalisat Mumbulsari 4 10 0.83 9 low 98 93 kmeans 2023 2020
135 350927 350923 Kalisat Mumbulsari 5 17 1.42 18 low 97 88 kmeans 2022 2021
136 350927 350923 Kalisat Mumbulsari 5 2 0.17 2 low 97 99 kmeans 2024 2022
137 350928 350923 Ledokombo Mumbulsari 5 2 0.17 2 low 97 99 kmeans 2024 2023
138 350928 350923 Ledokombo Mumbulsari 18 4 0.33 4 low 88 98 kmeans 2021 2024
139 350928 350923 Ledokombo Mumbulsari 10 68 5.67 35 low medium 93 52 kmeans 2020 2025
140 350928 350924 Ledokombo Pakusari 10 11 0.92 12 low 93 kmeans 2022 2020
141 350928 350924 Ledokombo Pakusari 4 21 1.75 21 low 98 85 kmeans 2023 2021
142 350929 350924 Sukowono Pakusari 6 10 0.83 10 low 96 93 kmeans 2023 2022
143 350929 350924 Sukowono Pakusari 7 0.58 7 low 95 kmeans 2022 2023
144 350929 350924 Sukowono Pakusari 11 3 0.25 3 low 93 98 kmeans 2020 2024
145 350929 350924 Sukowono Pakusari 20 61 5.08 43 low medium 86 57 kmeans 2021 2025
146 350929 350925 Sukowono Jelbuk 2 12 1 11 low 99 92 kmeans 2024 2020
147 350930 350925 Silo Jelbuk 68 21 1.75 21 medium low 52 85 kmeans 2024 2021
148 350930 350925 Silo Jelbuk 86 6 0.5 6 medium low 39 96 kmeans 2022
149 350930 350925 Silo Jelbuk 81 3 0.25 3 medium low 43 98 kmeans 2023
150 350930 350925 Silo Jelbuk 89 0 0 0 medium low 37 100 kmeans 2021 2024
151 350930 350925 Silo Jelbuk 109 102 8.5 76 medium 23 28 kmeans 2020 2025
152 350931 350926 Sumberjambe Mayang 7 16 1.33 15 low 95 89 kmeans 2020
153 350931 350926 Sumberjambe Mayang 19 18 1.5 19 low 87 88 kmeans 2021
154 350931 350926 Sumberjambe Mayang 8 6 0.5 6 low 95 96 kmeans 2023 2022
155 350931 350926 Sumberjambe Mayang 5 7 0.58 7 low 97 95 kmeans 2022 2023
156 350931 350926 Sumberjambe Mayang 4 0.33 4 low 98 kmeans 2024
157 350926 Mayang 90 7.5 46 medium 36 kmeans 2025
158 350927 Kalisat 16 1.33 15 low 89 kmeans 2020
159 350927 Kalisat 15 1.25 16 low 90 kmeans 2021
160 350927 Kalisat 5 0.42 5 low 97 kmeans 2022
161 350927 Kalisat 4 0.33 4 low 98 kmeans 2023
162 350927 Kalisat 5 0.42 5 low 97 kmeans 2024
163 350927 Kalisat 67 5.58 46 medium 53 kmeans 2025
164 350928 Ledokombo 10 0.83 8 low 93 kmeans 2020
165 350928 Ledokombo 18 1.5 18 low 88 kmeans 2021
166 350928 Ledokombo 10 0.83 10 low 93 kmeans 2022
167 350928 Ledokombo 4 0.33 4 low 98 kmeans 2023
168 350928 Ledokombo 5 0.42 5 low 97 kmeans 2024
169 350928 Ledokombo 71 5.92 46 medium 50 kmeans 2025
170 350929 Sukowono 11 0.92 11 low 93 kmeans 2020
171 350929 Sukowono 20 1.67 20 low 86 kmeans 2021
172 350929 Sukowono 7 0.58 7 low 95 kmeans 2022
173 350929 Sukowono 6 0.5 6 low 96 kmeans 2023
174 350929 Sukowono 2 0.17 3 low 99 kmeans 2024
175 350929 Sukowono 66 5.5 30 medium 53 kmeans 2025
176 350930 Silo 109 9.08 92 medium 23 kmeans 2020
177 350930 Silo 89 7.42 79 medium 37 kmeans 2021
178 350930 Silo 86 7.17 80 medium 39 kmeans 2022
179 350930 Silo 81 6.75 68 medium 43 kmeans 2023
180 350930 Silo 68 5.67 62 medium 52 kmeans 2024
181 350930 Silo 89 7.42 27 medium 37 kmeans 2025
182 350931 Sumberjambe 7 0.58 8 low 95 kmeans 2020
183 350931 Sumberjambe 19 1.58 19 low 87 kmeans 2021
184 350931 Sumberjambe 5 0.42 5 low 97 kmeans 2022
185 350931 Sumberjambe 8 0.67 8 low 95 kmeans 2023
186 350931 Sumberjambe 4 0.33 4 low 98 kmeans 2024
187 350931 Sumberjambe 98 8.17 56 medium 30 kmeans 2025

View File

@ -0,0 +1,156 @@
district_id,district_name,number_of_crime,crime_cleared,avg_crime,level,score,method,year
350901,Jombang,16,14,1.33,low,86,kmeans,2020
350901,Jombang,32,28,2.67,low,71,kmeans,2021
350901,Jombang,23,21,1.92,low,79,kmeans,2022
350901,Jombang,26,26,2.17,low,77,kmeans,2023
350901,Jombang,21,21,1.75,low,81,kmeans,2024
350902,Kencong,11,9,0.92,low,90,kmeans,2020
350902,Kencong,18,14,1.50,low,84,kmeans,2021
350902,Kencong,17,15,1.42,low,85,kmeans,2022
350902,Kencong,21,17,1.75,low,81,kmeans,2023
350902,Kencong,24,19,2.00,low,78,kmeans,2024
350903,Sumberbaru,51,35,4.25,high,54,kmeans,2020
350903,Sumberbaru,37,30,3.08,high,67,kmeans,2021
350903,Sumberbaru,23,23,1.92,high,79,kmeans,2022
350903,Sumberbaru,23,21,1.92,high,79,kmeans,2023
350903,Sumberbaru,23,21,1.92,high,79,kmeans,2024
350904,Gumukmas,16,15,1.33,low,86,kmeans,2020
350904,Gumukmas,27,20,2.25,low,76,kmeans,2021
350904,Gumukmas,18,13,1.50,low,84,kmeans,2022
350904,Gumukmas,10,10,0.83,low,91,kmeans,2023
350904,Gumukmas,20,20,1.67,low,82,kmeans,2024
350905,Umbulsari,28,23,2.33,high,75,kmeans,2020
350905,Umbulsari,26,17,2.17,low,77,kmeans,2021
350905,Umbulsari,24,12,2.00,low,78,kmeans,2022
350905,Umbulsari,21,20,1.75,low,81,kmeans,2023
350905,Umbulsari,16,16,1.33,low,86,kmeans,2024
350906,Tanggul,61,43,5.08,high,45,kmeans,2020
350906,Tanggul,58,42,4.83,high,47,kmeans,2021
350906,Tanggul,61,45,5.08,high,45,kmeans,2022
350906,Tanggul,55,55,4.58,high,50,kmeans,2023
350906,Tanggul,31,28,2.58,high,72,kmeans,2024
350907,Semboro,34,32,2.83,low,69,kmeans,2020
350907,Semboro,23,21,1.92,low,79,kmeans,2021
350907,Semboro,19,19,1.58,low,83,kmeans,2022
350907,Semboro,9,9,0.75,low,92,kmeans,2023
350907,Semboro,9,8,0.75,low,92,kmeans,2024
350908,Puger,43,43,3.58,high,61,kmeans,2020
350908,Puger,57,49,4.75,high,48,kmeans,2021
350908,Puger,33,27,2.75,high,70,kmeans,2022
350908,Puger,26,21,2.17,high,77,kmeans,2023
350908,Puger,21,20,1.75,high,81,kmeans,2024
350909,Bangsalsari,41,38,3.42,high,63,kmeans,2020
350909,Bangsalsari,41,24,3.42,high,63,kmeans,2021
350909,Bangsalsari,34,32,2.83,high,69,kmeans,2022
350909,Bangsalsari,13,13,1.08,low,89,kmeans,2023
350909,Bangsalsari,25,25,2.08,high,78,kmeans,2024
350910,Balung,56,47,4.67,high,49,kmeans,2020
350910,Balung,74,62,6.17,high,33,kmeans,2021
350910,Balung,54,43,4.50,high,51,kmeans,2022
350910,Balung,55,43,4.58,high,50,kmeans,2023
350910,Balung,39,28,3.25,high,65,kmeans,2024
350911,Wuluhan,48,41,4.00,high,56,kmeans,2020
350911,Wuluhan,70,49,5.83,high,36,kmeans,2021
350911,Wuluhan,45,37,3.75,high,59,kmeans,2022
350911,Wuluhan,26,25,2.17,high,77,kmeans,2023
350911,Wuluhan,27,24,2.25,high,76,kmeans,2024
350912,Ambulu,31,22,2.58,high,72,kmeans,2020
350912,Ambulu,39,27,3.25,high,65,kmeans,2021
350912,Ambulu,26,25,2.17,high,77,kmeans,2022
350912,Ambulu,31,28,2.58,high,72,kmeans,2023
350912,Ambulu,30,22,2.50,high,73,kmeans,2024
350913,Rambipuji,96,57,8.00,high,12,kmeans,2020
350913,Rambipuji,109,60,9.08,high,0,kmeans,2021
350913,Rambipuji,32,23,2.67,low,71,kmeans,2022
350913,Rambipuji,20,12,1.67,low,82,kmeans,2023
350913,Rambipuji,21,18,1.75,low,81,kmeans,2024
350914,Panti,56,30,4.67,high,49,kmeans,2020
350914,Panti,28,24,2.33,low,75,kmeans,2021
350914,Panti,22,25,1.83,low,80,kmeans,2022
350914,Panti,24,23,2.00,low,78,kmeans,2023
350914,Panti,9,7,0.75,low,92,kmeans,2024
350915,Sukorambi,14,13,1.17,low,88,kmeans,2020
350915,Sukorambi,27,19,2.25,low,76,kmeans,2021
350915,Sukorambi,14,7,1.17,low,88,kmeans,2022
350915,Sukorambi,18,12,1.50,low,84,kmeans,2023
350915,Sukorambi,4,4,0.33,low,97,kmeans,2024
350916,Jenggawah,55,51,4.58,high,50,kmeans,2020
350916,Jenggawah,51,49,4.25,high,54,kmeans,2021
350916,Jenggawah,34,31,2.83,low,69,kmeans,2022
350916,Jenggawah,57,57,4.75,high,48,kmeans,2023
350916,Jenggawah,38,36,3.17,high,66,kmeans,2024
350917,Ajung,0,0,0.00,low,100,kmeans,2020
350917,Ajung,16,10,1.33,low,86,kmeans,2021
350917,Ajung,23,22,1.92,low,79,kmeans,2022
350917,Ajung,24,20,2.00,low,78,kmeans,2023
350917,Ajung,25,25,2.08,low,78,kmeans,2024
350918,Tempurejo,19,14,1.58,low,83,kmeans,2020
350918,Tempurejo,14,12,1.17,low,88,kmeans,2021
350918,Tempurejo,18,11,1.50,low,84,kmeans,2022
350918,Tempurejo,10,5,0.83,low,91,kmeans,2023
350918,Tempurejo,13,6,1.08,low,89,kmeans,2024
350919,Kaliwates,56,37,4.67,medium,49,kmeans,2020
350919,Kaliwates,63,43,5.25,medium,43,kmeans,2021
350919,Kaliwates,36,28,3.00,medium,67,kmeans,2022
350919,Kaliwates,23,15,1.92,medium,79,kmeans,2023
350919,Kaliwates,16,16,1.33,medium,86,kmeans,2024
350920,Patrang,46,33,3.83,medium,58,kmeans,2020
350920,Patrang,88,54,7.33,medium,20,kmeans,2021
350920,Patrang,42,37,3.50,medium,62,kmeans,2022
350920,Patrang,16,13,1.33,medium,86,kmeans,2023
350920,Patrang,10,8,0.83,medium,91,kmeans,2024
350921,Sumbersari,38,28,3.17,high,66,kmeans,2020
350921,Sumbersari,52,37,4.33,medium,53,kmeans,2021
350921,Sumbersari,59,43,4.92,medium,46,kmeans,2022
350921,Sumbersari,35,18,2.92,medium,68,kmeans,2023
350921,Sumbersari,33,12,2.75,medium,70,kmeans,2024
350922,Arjasa,29,20,2.42,low,74,kmeans,2020
350922,Arjasa,47,30,3.92,low,57,kmeans,2021
350922,Arjasa,19,14,1.58,low,83,kmeans,2022
350922,Arjasa,10,9,0.83,low,91,kmeans,2023
350922,Arjasa,11,8,0.92,low,90,kmeans,2024
350923,Mumbulsari,28,27,2.33,high,75,kmeans,2020
350923,Mumbulsari,27,19,2.25,low,76,kmeans,2021
350923,Mumbulsari,23,17,1.92,low,79,kmeans,2022
350923,Mumbulsari,11,11,0.92,low,90,kmeans,2023
350923,Mumbulsari,10,7,0.83,low,91,kmeans,2024
350924,Pakusari,39,29,3.25,low,65,kmeans,2020
350924,Pakusari,52,44,4.33,low,53,kmeans,2021
350924,Pakusari,34,33,2.83,low,69,kmeans,2022
350924,Pakusari,12,12,1.00,low,89,kmeans,2023
350924,Pakusari,15,11,1.25,low,87,kmeans,2024
350925,Jelbuk,32,29,2.67,low,71,kmeans,2020
350925,Jelbuk,55,30,4.58,low,50,kmeans,2021
350925,Jelbuk,16,10,1.33,low,86,kmeans,2022
350925,Jelbuk,16,15,1.33,low,86,kmeans,2023
350925,Jelbuk,13,6,1.08,low,89,kmeans,2024
350926,Mayang,11,6,0.92,low,90,kmeans,2020
350926,Mayang,35,19,2.92,low,68,kmeans,2021
350926,Mayang,20,19,1.67,low,82,kmeans,2022
350926,Mayang,13,7,1.08,low,89,kmeans,2023
350926,Mayang,10,9,0.83,low,91,kmeans,2024
350927,Kalisat,84,47,7.00,high,23,kmeans,2020
350927,Kalisat,95,58,7.92,high,13,kmeans,2021
350927,Kalisat,68,36,5.67,high,38,kmeans,2022
350927,Kalisat,13,12,1.08,low,89,kmeans,2023
350927,Kalisat,10,10,0.83,low,91,kmeans,2024
350928,Ledokombo,22,12,1.83,low,80,kmeans,2020
350928,Ledokombo,45,34,3.75,low,59,kmeans,2021
350928,Ledokombo,17,12,1.42,low,85,kmeans,2022
350928,Ledokombo,13,13,1.08,low,89,kmeans,2023
350928,Ledokombo,6,5,0.50,low,95,kmeans,2024
350929,Sukowono,38,27,3.17,high,66,kmeans,2020
350929,Sukowono,46,32,3.83,low,58,kmeans,2021
350929,Sukowono,34,29,2.83,low,69,kmeans,2022
350929,Sukowono,25,18,2.08,low,78,kmeans,2023
350929,Sukowono,28,19,2.33,low,75,kmeans,2024
350930,Silo,26,15,2.17,high,77,kmeans,2020
350930,Silo,39,20,3.25,high,65,kmeans,2021
350930,Silo,28,18,2.33,high,75,kmeans,2022
350930,Silo,29,16,2.42,high,74,kmeans,2023
350930,Silo,21,16,1.75,low,81,kmeans,2024
350931,Sumberjambe,55,46,4.58,high,50,kmeans,2020
350931,Sumberjambe,20,19,1.67,low,82,kmeans,2021
350931,Sumberjambe,12,12,1.00,low,89,kmeans,2022
350931,Sumberjambe,16,11,1.33,low,86,kmeans,2023
350931,Sumberjambe,6,6,0.50,low,95,kmeans,2024
1 district_id district_name number_of_crime crime_cleared avg_crime level score method year
2 350901 Jombang 16 14 1.33 low 86 kmeans 2020
3 350901 Jombang 32 28 2.67 low 71 kmeans 2021
4 350901 Jombang 23 21 1.92 low 79 kmeans 2022
5 350901 Jombang 26 26 2.17 low 77 kmeans 2023
6 350901 Jombang 21 21 1.75 low 81 kmeans 2024
7 350902 Kencong 11 9 0.92 low 90 kmeans 2020
8 350902 Kencong 18 14 1.50 low 84 kmeans 2021
9 350902 Kencong 17 15 1.42 low 85 kmeans 2022
10 350902 Kencong 21 17 1.75 low 81 kmeans 2023
11 350902 Kencong 24 19 2.00 low 78 kmeans 2024
12 350903 Sumberbaru 51 35 4.25 high 54 kmeans 2020
13 350903 Sumberbaru 37 30 3.08 high 67 kmeans 2021
14 350903 Sumberbaru 23 23 1.92 high 79 kmeans 2022
15 350903 Sumberbaru 23 21 1.92 high 79 kmeans 2023
16 350903 Sumberbaru 23 21 1.92 high 79 kmeans 2024
17 350904 Gumukmas 16 15 1.33 low 86 kmeans 2020
18 350904 Gumukmas 27 20 2.25 low 76 kmeans 2021
19 350904 Gumukmas 18 13 1.50 low 84 kmeans 2022
20 350904 Gumukmas 10 10 0.83 low 91 kmeans 2023
21 350904 Gumukmas 20 20 1.67 low 82 kmeans 2024
22 350905 Umbulsari 28 23 2.33 high 75 kmeans 2020
23 350905 Umbulsari 26 17 2.17 low 77 kmeans 2021
24 350905 Umbulsari 24 12 2.00 low 78 kmeans 2022
25 350905 Umbulsari 21 20 1.75 low 81 kmeans 2023
26 350905 Umbulsari 16 16 1.33 low 86 kmeans 2024
27 350906 Tanggul 61 43 5.08 high 45 kmeans 2020
28 350906 Tanggul 58 42 4.83 high 47 kmeans 2021
29 350906 Tanggul 61 45 5.08 high 45 kmeans 2022
30 350906 Tanggul 55 55 4.58 high 50 kmeans 2023
31 350906 Tanggul 31 28 2.58 high 72 kmeans 2024
32 350907 Semboro 34 32 2.83 low 69 kmeans 2020
33 350907 Semboro 23 21 1.92 low 79 kmeans 2021
34 350907 Semboro 19 19 1.58 low 83 kmeans 2022
35 350907 Semboro 9 9 0.75 low 92 kmeans 2023
36 350907 Semboro 9 8 0.75 low 92 kmeans 2024
37 350908 Puger 43 43 3.58 high 61 kmeans 2020
38 350908 Puger 57 49 4.75 high 48 kmeans 2021
39 350908 Puger 33 27 2.75 high 70 kmeans 2022
40 350908 Puger 26 21 2.17 high 77 kmeans 2023
41 350908 Puger 21 20 1.75 high 81 kmeans 2024
42 350909 Bangsalsari 41 38 3.42 high 63 kmeans 2020
43 350909 Bangsalsari 41 24 3.42 high 63 kmeans 2021
44 350909 Bangsalsari 34 32 2.83 high 69 kmeans 2022
45 350909 Bangsalsari 13 13 1.08 low 89 kmeans 2023
46 350909 Bangsalsari 25 25 2.08 high 78 kmeans 2024
47 350910 Balung 56 47 4.67 high 49 kmeans 2020
48 350910 Balung 74 62 6.17 high 33 kmeans 2021
49 350910 Balung 54 43 4.50 high 51 kmeans 2022
50 350910 Balung 55 43 4.58 high 50 kmeans 2023
51 350910 Balung 39 28 3.25 high 65 kmeans 2024
52 350911 Wuluhan 48 41 4.00 high 56 kmeans 2020
53 350911 Wuluhan 70 49 5.83 high 36 kmeans 2021
54 350911 Wuluhan 45 37 3.75 high 59 kmeans 2022
55 350911 Wuluhan 26 25 2.17 high 77 kmeans 2023
56 350911 Wuluhan 27 24 2.25 high 76 kmeans 2024
57 350912 Ambulu 31 22 2.58 high 72 kmeans 2020
58 350912 Ambulu 39 27 3.25 high 65 kmeans 2021
59 350912 Ambulu 26 25 2.17 high 77 kmeans 2022
60 350912 Ambulu 31 28 2.58 high 72 kmeans 2023
61 350912 Ambulu 30 22 2.50 high 73 kmeans 2024
62 350913 Rambipuji 96 57 8.00 high 12 kmeans 2020
63 350913 Rambipuji 109 60 9.08 high 0 kmeans 2021
64 350913 Rambipuji 32 23 2.67 low 71 kmeans 2022
65 350913 Rambipuji 20 12 1.67 low 82 kmeans 2023
66 350913 Rambipuji 21 18 1.75 low 81 kmeans 2024
67 350914 Panti 56 30 4.67 high 49 kmeans 2020
68 350914 Panti 28 24 2.33 low 75 kmeans 2021
69 350914 Panti 22 25 1.83 low 80 kmeans 2022
70 350914 Panti 24 23 2.00 low 78 kmeans 2023
71 350914 Panti 9 7 0.75 low 92 kmeans 2024
72 350915 Sukorambi 14 13 1.17 low 88 kmeans 2020
73 350915 Sukorambi 27 19 2.25 low 76 kmeans 2021
74 350915 Sukorambi 14 7 1.17 low 88 kmeans 2022
75 350915 Sukorambi 18 12 1.50 low 84 kmeans 2023
76 350915 Sukorambi 4 4 0.33 low 97 kmeans 2024
77 350916 Jenggawah 55 51 4.58 high 50 kmeans 2020
78 350916 Jenggawah 51 49 4.25 high 54 kmeans 2021
79 350916 Jenggawah 34 31 2.83 low 69 kmeans 2022
80 350916 Jenggawah 57 57 4.75 high 48 kmeans 2023
81 350916 Jenggawah 38 36 3.17 high 66 kmeans 2024
82 350917 Ajung 0 0 0.00 low 100 kmeans 2020
83 350917 Ajung 16 10 1.33 low 86 kmeans 2021
84 350917 Ajung 23 22 1.92 low 79 kmeans 2022
85 350917 Ajung 24 20 2.00 low 78 kmeans 2023
86 350917 Ajung 25 25 2.08 low 78 kmeans 2024
87 350918 Tempurejo 19 14 1.58 low 83 kmeans 2020
88 350918 Tempurejo 14 12 1.17 low 88 kmeans 2021
89 350918 Tempurejo 18 11 1.50 low 84 kmeans 2022
90 350918 Tempurejo 10 5 0.83 low 91 kmeans 2023
91 350918 Tempurejo 13 6 1.08 low 89 kmeans 2024
92 350919 Kaliwates 56 37 4.67 medium 49 kmeans 2020
93 350919 Kaliwates 63 43 5.25 medium 43 kmeans 2021
94 350919 Kaliwates 36 28 3.00 medium 67 kmeans 2022
95 350919 Kaliwates 23 15 1.92 medium 79 kmeans 2023
96 350919 Kaliwates 16 16 1.33 medium 86 kmeans 2024
97 350920 Patrang 46 33 3.83 medium 58 kmeans 2020
98 350920 Patrang 88 54 7.33 medium 20 kmeans 2021
99 350920 Patrang 42 37 3.50 medium 62 kmeans 2022
100 350920 Patrang 16 13 1.33 medium 86 kmeans 2023
101 350920 Patrang 10 8 0.83 medium 91 kmeans 2024
102 350921 Sumbersari 38 28 3.17 high 66 kmeans 2020
103 350921 Sumbersari 52 37 4.33 medium 53 kmeans 2021
104 350921 Sumbersari 59 43 4.92 medium 46 kmeans 2022
105 350921 Sumbersari 35 18 2.92 medium 68 kmeans 2023
106 350921 Sumbersari 33 12 2.75 medium 70 kmeans 2024
107 350922 Arjasa 29 20 2.42 low 74 kmeans 2020
108 350922 Arjasa 47 30 3.92 low 57 kmeans 2021
109 350922 Arjasa 19 14 1.58 low 83 kmeans 2022
110 350922 Arjasa 10 9 0.83 low 91 kmeans 2023
111 350922 Arjasa 11 8 0.92 low 90 kmeans 2024
112 350923 Mumbulsari 28 27 2.33 high 75 kmeans 2020
113 350923 Mumbulsari 27 19 2.25 low 76 kmeans 2021
114 350923 Mumbulsari 23 17 1.92 low 79 kmeans 2022
115 350923 Mumbulsari 11 11 0.92 low 90 kmeans 2023
116 350923 Mumbulsari 10 7 0.83 low 91 kmeans 2024
117 350924 Pakusari 39 29 3.25 low 65 kmeans 2020
118 350924 Pakusari 52 44 4.33 low 53 kmeans 2021
119 350924 Pakusari 34 33 2.83 low 69 kmeans 2022
120 350924 Pakusari 12 12 1.00 low 89 kmeans 2023
121 350924 Pakusari 15 11 1.25 low 87 kmeans 2024
122 350925 Jelbuk 32 29 2.67 low 71 kmeans 2020
123 350925 Jelbuk 55 30 4.58 low 50 kmeans 2021
124 350925 Jelbuk 16 10 1.33 low 86 kmeans 2022
125 350925 Jelbuk 16 15 1.33 low 86 kmeans 2023
126 350925 Jelbuk 13 6 1.08 low 89 kmeans 2024
127 350926 Mayang 11 6 0.92 low 90 kmeans 2020
128 350926 Mayang 35 19 2.92 low 68 kmeans 2021
129 350926 Mayang 20 19 1.67 low 82 kmeans 2022
130 350926 Mayang 13 7 1.08 low 89 kmeans 2023
131 350926 Mayang 10 9 0.83 low 91 kmeans 2024
132 350927 Kalisat 84 47 7.00 high 23 kmeans 2020
133 350927 Kalisat 95 58 7.92 high 13 kmeans 2021
134 350927 Kalisat 68 36 5.67 high 38 kmeans 2022
135 350927 Kalisat 13 12 1.08 low 89 kmeans 2023
136 350927 Kalisat 10 10 0.83 low 91 kmeans 2024
137 350928 Ledokombo 22 12 1.83 low 80 kmeans 2020
138 350928 Ledokombo 45 34 3.75 low 59 kmeans 2021
139 350928 Ledokombo 17 12 1.42 low 85 kmeans 2022
140 350928 Ledokombo 13 13 1.08 low 89 kmeans 2023
141 350928 Ledokombo 6 5 0.50 low 95 kmeans 2024
142 350929 Sukowono 38 27 3.17 high 66 kmeans 2020
143 350929 Sukowono 46 32 3.83 low 58 kmeans 2021
144 350929 Sukowono 34 29 2.83 low 69 kmeans 2022
145 350929 Sukowono 25 18 2.08 low 78 kmeans 2023
146 350929 Sukowono 28 19 2.33 low 75 kmeans 2024
147 350930 Silo 26 15 2.17 high 77 kmeans 2020
148 350930 Silo 39 20 3.25 high 65 kmeans 2021
149 350930 Silo 28 18 2.33 high 75 kmeans 2022
150 350930 Silo 29 16 2.42 high 74 kmeans 2023
151 350930 Silo 21 16 1.75 low 81 kmeans 2024
152 350931 Sumberjambe 55 46 4.58 high 50 kmeans 2020
153 350931 Sumberjambe 20 19 1.67 low 82 kmeans 2021
154 350931 Sumberjambe 12 12 1.00 low 89 kmeans 2022
155 350931 Sumberjambe 16 11 1.33 low 86 kmeans 2023
156 350931 Sumberjambe 6 6 0.50 low 95 kmeans 2024

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
export const districtCenters = [
{ kecamatan: "Sumbersari", lat: -8.170662, lng: 113.727582 },
{ kecamatan: "Wuluhan", lat: -8.365478, lng: 113.537137 },
{ kecamatan: "Bangsalsari", lat: -8.2013, lng: 113.5323 },
{ kecamatan: "Kaliwates", lat: -8.1725, lng: 113.7000 },
{ kecamatan: "Puger", lat: -8.477942, lng: 113.370447 },
{ kecamatan: "Ambulu", lat: -8.381355, lng: 113.608561 },
{ kecamatan: "Silo", lat: -8.22917, lng: 113.86500 },
{ kecamatan: "Sumberbaru", lat: -8.119229, lng: 113.392973 },
{ kecamatan: "Patrang", lat: -8.14611, lng: 113.71250 },
{ kecamatan: "Tanggul", lat: -8.161572, lng: 113.451239 },
{ kecamatan: "Jenggawah", lat: -8.296967, lng: 113.656173 },
{ kecamatan: "Gumukmas", lat: -8.32306, lng: 113.40667 },
{ kecamatan: "Rambipuji", lat: -8.210581, lng: 113.603610 },
{ kecamatan: "Ajung", lat: -8.245360, lng: 113.653218 },
{ kecamatan: "Balung", lat: -8.26861, lng: 113.52667 },
{ kecamatan: "Tempurejo", lat: -8.4000, lng: 113.8000 },
{ kecamatan: "Kalisat", lat: -8.1500, lng: 113.8000 },
{ kecamatan: "Umbulsari", lat: -8.2500, lng: 113.5000 },
{ kecamatan: "Kencong", lat: -8.3500, lng: 113.4000 },
{ kecamatan: "Ledokombo", lat: -8.2000, lng: 113.8000 },
{ kecamatan: "Mumbulsari", lat: -8.3000, lng: 113.6000 },
{ kecamatan: "Panti", lat: -8.1500, lng: 113.6000 },
{ kecamatan: "Sumberjambe", lat: -8.1000, lng: 113.8000 },
{ kecamatan: "Sukowono", lat: -8.2000, lng: 113.7000 },
{ kecamatan: "Mayang", lat: -8.2500, lng: 113.7000 },
{ kecamatan: "Semboro", lat: -8.3000, lng: 113.5000 },
{ kecamatan: "Jombang", lat: -8.3500, lng: 113.5000 },
{ kecamatan: "Pakusari", lat: -8.2000, lng: 113.7000 },
{ kecamatan: "Arjasa", lat: -8.1500, lng: 113.7000 },
{ kecamatan: "Sukorambi", lat: -8.2000, lng: 113.6000 },
{ kecamatan: "Jelbuk", lat: -8.1000, lng: 113.7000 },
];

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "crimes" ALTER COLUMN "year" DROP NOT NULL;

View File

@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "test" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" VARCHAR(100) NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "test_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "idx_test_name" ON "test"("name");

View File

@ -0,0 +1,11 @@
/*
Warnings:
- You are about to drop the `test` table. If the table is not empty, all the data it contains will be lost.
*/
-- AlterTable
ALTER TABLE "crimes" ADD COLUMN "crime_cleared" INTEGER NOT NULL DEFAULT 0;
-- DropTable
DROP TABLE "test";

View File

@ -0,0 +1,12 @@
/*
Warnings:
- Added the required column `city_id` to the `units` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "units" ADD COLUMN "city_id" VARCHAR(20) NOT NULL,
ALTER COLUMN "district_id" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "units" ADD CONSTRAINT "units_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "cities"("id") ON DELETE CASCADE ON UPDATE NO ACTION;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "units" ADD COLUMN "phone" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "crimes" ADD COLUMN "avg_crime" DOUBLE PRECISION NOT NULL DEFAULT 0;

View File

@ -125,6 +125,7 @@ model cities {
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
districts districts[]
units units[]
@@index([name], map: "idx_cities_name")
}
@ -172,9 +173,11 @@ model crimes {
method String? @db.VarChar(100)
month Int?
number_of_crime Int @default(0)
crime_cleared Int @default(0)
avg_crime Float @default(0)
score Float @default(0)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
year Int
year Int?
source_type String? @db.VarChar(100)
crime_incidents crime_incidents[]
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
@ -265,19 +268,22 @@ model incident_logs {
model units {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
code_unit String @unique @db.VarChar(20)
district_id String @unique @db.VarChar(20)
district_id String? @unique @db.VarChar(20)
city_id String @db.VarChar(20)
name String @db.VarChar(100)
description String?
type unit_type
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
address String?
phone String?
land_area Float?
latitude Float
longitude Float
location Unsupported("geography")
unit_statistics unit_statistics[]
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
districts districts? @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@index([name], map: "idx_units_name")
@@index([type], map: "idx_units_type")

View File

@ -10,7 +10,8 @@ import { CrimeCategoriesSeeder } from './seeds/crime-category';
import { UnitSeeder } from './seeds/units';
import { CrimesSeeder } from './seeds/crimes';
import { CrimeIncidentsSeeder as DetailedCrimeIncidentsSeeder } from './seeds/crime-incidents';
import { CrimeIncidentsByUnitSeeder } from './seeds/crime-incidents';
import { CrimeIncidentsByTypeSeeder } from './seeds/crime-incidents-cbt';
const prisma = new PrismaClient();
@ -37,8 +38,8 @@ class DatabaseSeeder {
new UnitSeeder(prisma),
new DemographicsSeeder(prisma),
new CrimesSeeder(prisma),
new DetailedCrimeIncidentsSeeder(prisma), // Add the new crime incidents seeder
// new CrimeIncidentsSeederV2(prisma),
// new CrimeIncidentsByUnitSeeder(prisma),
new CrimeIncidentsByTypeSeeder(prisma),
];
}

View File

@ -0,0 +1,779 @@
import {
crime_incidents,
crime_rates,
crime_status,
crimes,
locations,
PrismaClient,
} from "@prisma/client";
import { generateId, generateIdWithDbCounter } from "../../app/_utils/common";
import { createClient } from "../../app/_utils/supabase/client";
import * as fs from "fs";
import * as path from "path";
import { CRegex } from "../../app/_utils/const/regex";
import { districtCenters } from "../data/jsons/district-center";
type ICreateLocations = {
id: string;
created_at: Date;
updated_at: Date;
district_id: string;
event_id: string;
address: string;
type: string;
latitude: number;
longitude: number;
land_area: number;
location: string;
};
// New type for crime data structure from JSON
interface CrimeData {
district_id: string;
district_name: string;
month: string;
year: number;
[key: string]: string | number; // For dynamic crime category fields ending with "_crimes"
crime_cleared: number;
number_of_crime: number;
}
export class CrimeIncidentsByTypeSeeder {
private crimeMonthlyData: Map<
string,
{
number_of_crime: number;
crime_cleared: number;
categories: { [category: string]: number };
}
> = new Map();
private monthNameMap: { [key: string]: number } = {
"JAN": 1,
"FEB": 2,
"MAR": 3,
"APR": 4,
"MAY": 5,
"JUN": 6,
"JUL": 7,
"AUG": 8,
"SEP": 9,
"OCT": 10,
"NOV": 11,
"DEC": 12,
};
constructor(
private prisma: PrismaClient,
private supabase = createClient(),
) {}
private async loadCrimeMonthlyData(): Promise<void> {
const jsonFilePath = path.resolve(
__dirname,
"../data/jsons/crimes/cbt.json",
);
return new Promise((resolve, reject) => {
fs.readFile(jsonFilePath, "utf8", (err, data) => {
if (err) {
console.error(
"Error reading crime monthly JSON data:",
err,
);
reject(err);
return;
}
try {
const crimeData: CrimeData[] = JSON.parse(data);
for (const row of crimeData) {
const monthNum = this.monthNameToNumber(row.month);
const key =
`${row.district_id}-${monthNum}-${row.year}`;
// Extract crime categories (fields ending with "_crimes")
const categories: { [category: string]: number } = {};
for (const [field, value] of Object.entries(row)) {
if (
field.endsWith("_crimes") &&
typeof value === "number"
) {
// Extract category name from field name (remove "_crimes" suffix)
const categoryName = field.slice(0, -7);
categories[categoryName] = value;
}
}
this.crimeMonthlyData.set(key, {
number_of_crime: row.number_of_crime as number,
crime_cleared: row.crime_cleared as number,
categories: categories,
});
}
resolve();
} catch (error) {
console.error(
"Error parsing crime monthly JSON data:",
error,
);
reject(error);
}
});
});
}
private monthNameToNumber(monthName: string): number {
const month = this.monthNameMap[monthName.toUpperCase()];
if (!month) {
console.warn(
`Unknown month name: ${monthName}, defaulting to January`,
);
return 1;
}
return month;
}
/**
* Generates mock incidents for 2025 data
*/
private async generateMock2025Incidents(): Promise<void> {
const crimes2025 = await this.prisma.crimes.findMany({
where: {
year: 2025,
month: { not: null },
source_type: "cbt",
},
orderBy: [{ district_id: "asc" }, { month: "asc" }],
});
if (crimes2025.length === 0) {
return;
}
const crimeCategories = await this.prisma.crime_categories.findMany();
let totalIncidentsCreated = 0;
let totalMatched = 0;
let totalMismatched = 0;
let totalResolved = 0;
let districtsProcessed = new Set();
for (const crimeRecord of crimes2025) {
if (
!crimeRecord.number_of_crime || crimeRecord.number_of_crime <= 0
) {
continue;
}
const incidents = await this.createMockIncidentsForCrime(
crimeRecord,
crimeCategories,
);
totalIncidentsCreated += incidents.length;
districtsProcessed.add(crimeRecord.district_id);
totalResolved += incidents.filter((inc) =>
inc.status === "resolved"
).length;
totalMatched += Math.min(3, Math.floor(Math.random() * 5));
}
console.log("\n📊 2025 Mock Data Summary:");
console.log(`├─ Total incidents created: ${totalIncidentsCreated}`);
console.log(`├─ Districts processed: ${districtsProcessed.size}`);
console.log(
`├─ Categories: ${totalMatched} matched, ${totalMismatched} mismatched`,
);
console.log(`└─ Total resolved cases: ${totalResolved}`);
}
private async createMockIncidentsForCrime(
crime: crimes,
allCategories: any[],
): Promise<any[]> {
const incidentsCreated = [];
const district = await this.prisma.districts.findUnique({
where: { id: crime.district_id },
include: { cities: true },
});
if (!district) {
return [];
}
const geo = await this.prisma.geographics.findFirst({
where: {
district_id: district.id,
year: crime.year ?? 2025,
},
orderBy: {
year: "desc",
},
select: {
latitude: true,
longitude: true,
land_area: true,
},
});
if (!geo) {
return [];
}
const numberOfCrimes = crime.number_of_crime || 10;
const crimesCleared = Math.floor(
numberOfCrimes * (0.3 + Math.random() * 0.4),
);
const locationPool = this.generateDistributedPoints(
geo.land_area!,
numberOfCrimes,
district.id,
district.name,
);
const jemberStreets = [
"Jalan Pahlawan",
"Jalan Merdeka",
"Jalan Cendrawasih",
"Jalan Srikandi",
"Jalan Sumbermujur",
"Jalan Taman Siswa",
"Jalan Pantai",
"Jalan Raya Sumberbaru",
"Jalan Abdurrahman Saleh",
"Jalan Mastrip",
"Jalan PB Sudirman",
"Jalan Kalimantan",
"Jalan Sumatra",
"Jalan Jawa",
"Jalan Gajah Mada",
"Jalan Letjen Suprapto",
"Jalan Hayam Wuruk",
"Jalan Trunojoyo",
"Jalan Imam Bonjol",
"Jalan Diponegoro",
"Jalan Ahmad Yani",
"Jalan Kartini",
"Jalan Gatot Subroto",
];
const placeTypes = [
"Perumahan",
"Apartemen",
"Komplek",
"Pasar",
"Toko",
"Terminal",
"Stasiun",
"Kampus",
"Sekolah",
"Perkantoran",
"Pertokoan",
"Pusat Perbelanjaan",
"Taman",
"Alun-alun",
"Simpang",
"Pertigaan",
"Perempatan",
];
const user = await this.prisma.users.findFirst({
where: {
email: "sigapcompany@gmail.com",
},
select: {
id: true,
},
});
if (!user) {
return [];
}
const event = await this.prisma.events.findFirst({
where: {
user_id: user.id,
},
});
if (!event) {
return [];
}
const incidentsToCreate: Partial<crime_incidents>[] = [];
const locationsToCreate: Partial<locations>[] = [];
const categoryDistribution: { categoryId: string; count: number }[] =
[];
let remainingCrimes = numberOfCrimes;
const shuffledCategories = [...allCategories].sort(() =>
Math.random() - 0.5
);
const categoriesToUse = Math.max(
1,
Math.min(3, Math.floor(Math.random() * 5)),
);
for (
let i = 0;
i < categoriesToUse && i < shuffledCategories.length &&
remainingCrimes > 0;
i++
) {
const category = shuffledCategories[i];
if (
i === categoriesToUse - 1 || i === shuffledCategories.length - 1
) {
categoryDistribution.push({
categoryId: category.id,
count: remainingCrimes,
});
remainingCrimes = 0;
} else {
const count = Math.max(
1,
Math.floor(remainingCrimes * (0.2 + Math.random() * 0.6)),
);
categoryDistribution.push({
categoryId: category.id,
count: count,
});
remainingCrimes -= count;
}
}
let resolvedCount = 0;
for (const category of categoryDistribution) {
for (let i = 0; i < category.count; i++) {
const year = 2025;
const month = (crime.month as number) - 1;
const maxDay = new Date(year, month + 1, 0).getDate();
const day = Math.floor(Math.random() * maxDay) + 1;
const hour = Math.floor(Math.random() * 24);
const minute = Math.floor(Math.random() * 60);
const timestamp = new Date(year, month, day, hour, minute);
const randomLocationIndex = Math.floor(
Math.random() * locationPool.length,
);
const selectedLocation = locationPool[randomLocationIndex];
const streetName = jemberStreets[
Math.floor(Math.random() * jemberStreets.length)
];
const buildingNumber = Math.floor(Math.random() * 200) + 1;
const placeType =
placeTypes[Math.floor(Math.random() * placeTypes.length)];
let randomAddress;
const addressType = Math.floor(Math.random() * 3);
switch (addressType) {
case 0:
randomAddress =
`${streetName} No. ${buildingNumber}, ${district.name}, Jember`;
break;
case 1:
randomAddress =
`${placeType} ${district.name}, ${streetName}, Jember`;
break;
case 2:
randomAddress = `${streetName} Blok ${
String.fromCharCode(
65 + Math.floor(Math.random() * 26),
)
}-${
Math.floor(Math.random() * 20) + 1
}, ${district.name}, Jember`;
break;
}
const locationData: Partial<ICreateLocations> = {
district_id: district.id,
event_id: event.id,
address: randomAddress,
type: "crime incident",
latitude: selectedLocation.latitude,
longitude: selectedLocation.longitude,
location:
`POINT(${selectedLocation.longitude} ${selectedLocation.latitude})`,
};
locationsToCreate.push(locationData);
const incidentId = await generateIdWithDbCounter(
"crime_incidents",
{
prefix: "CI",
segments: {
codes: [district.city_id],
sequentialDigits: 4,
year,
},
format: "{prefix}-{codes}-{sequence}-{year}",
separator: "-",
uniquenessStrategy: "counter",
},
CRegex.CR_YEAR_SEQUENCE,
);
const status = resolvedCount < crimesCleared
? ("resolved" as crime_status)
: ("unresolved" as crime_status);
if (status === "resolved") {
resolvedCount++;
}
const categoryDetails = allCategories.find((c) =>
c.id === category.categoryId
);
const categoryName = categoryDetails.name;
const locs = [
`di daerah ${district.name}`,
`di sekitar ${district.name}`,
`di wilayah ${district.name}`,
`di jalan utama ${district.name}`,
`di perumahan ${district.name}`,
`di pasar ${district.name}`,
`di perbatasan ${district.name}`,
`di kawasan ${placeType.toLowerCase()} ${district.name}`,
`di persimpangan jalan ${streetName}`,
`di dekat ${placeType.toLowerCase()} ${district.name}`,
`di belakang ${placeType.toLowerCase()} ${district.name}`,
`di area ${streetName}`,
`di sekitar ${streetName} ${district.name}`,
`tidak jauh dari pusat ${district.name}`,
`di pinggiran ${district.name}`,
];
const randomLocation =
locs[Math.floor(Math.random() * locs.length)];
const descriptions = [
`Kasus ${categoryName.toLowerCase()} ${randomAddress}`,
`Laporan ${categoryName.toLowerCase()} terjadi pada ${timestamp} ${randomLocation}`,
`${categoryName} dilaporkan ${randomLocation}`,
`Insiden ${categoryName.toLowerCase()} terjadi ${randomLocation}`,
`Kejadian ${categoryName.toLowerCase()} ${randomLocation}`,
`${categoryName} terdeteksi ${randomLocation} pada ${timestamp.toLocaleTimeString()}`,
`Pelaporan ${categoryName.toLowerCase()} di ${randomAddress}`,
`Kasus ${categoryName.toLowerCase()} terjadi di ${streetName}`,
`${categoryName} terjadi di dekat ${placeType.toLowerCase()} ${district.name}`,
`Insiden ${categoryName.toLowerCase()} dilaporkan warga setempat ${randomLocation}`,
];
const randomDescription = descriptions[
Math.floor(Math.random() * descriptions.length)
];
incidentsToCreate.push({
id: incidentId,
crime_id: crime.id,
crime_category_id: category.categoryId,
location_id: undefined as string | undefined,
description: randomDescription,
victim_count: 0,
status: status,
timestamp: timestamp,
});
}
}
try {
await this.chunkedInsertLocations(locationsToCreate);
} catch (error) {
return [];
}
const createdLocations = await this.prisma.locations.findMany({
where: {
event_id: event.id,
district_id: district.id,
address: {
in: locationsToCreate
.map((loc) => loc.address)
.filter((address): address is string =>
address !== undefined
),
},
},
select: {
id: true,
address: true,
},
});
const addressToId = new Map<string, string>();
for (const loc of createdLocations) {
if (loc.address !== null) {
addressToId.set(loc.address, loc.id);
}
}
for (let i = 0; i < incidentsToCreate.length; i++) {
const address = locationsToCreate[i].address;
if (typeof address === "string") {
incidentsToCreate[i].location_id = addressToId.get(address);
}
}
await this.chunkedInsertIncidents(incidentsToCreate);
incidentsCreated.push(...incidentsToCreate);
return incidentsCreated;
}
private async chunkedInsertIncidents(data: any[], chunkSize: number = 200) {
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
await this.prisma.crime_incidents.createMany({
data: chunk,
skipDuplicates: true,
});
}
}
private async chunkedInsertLocations(
locations: any[],
chunkSize: number = 200,
) {
for (let i = 0; i < locations.length; i += chunkSize) {
const chunk = locations.slice(i, i + chunkSize);
let { error } = await this.supabase
.from("locations")
.insert(chunk)
.select();
if (error) {
throw error;
}
}
}
async run(): Promise<void> {
console.log("🌱 Seeding crime incidents data...");
try {
await this.loadCrimeMonthlyData();
const existingIncidents = await this.prisma.crime_incidents
.findFirst({
where: {
crimes: {
source_type: "cbt",
},
},
});
if (existingIncidents) {
const existing2025Incidents = await this.prisma.crime_incidents
.findFirst({
where: {
timestamp: {
gte: new Date("2025-01-01"),
lt: new Date("2026-01-01"),
},
},
});
if (!existing2025Incidents) {
await this.generateMock2025Incidents();
}
return;
}
const crimeRecords = await this.prisma.crimes.findMany({
where: {
month: { not: null },
number_of_crime: { gt: 0 },
source_type: "cbt",
year: { lt: 2025 },
},
orderBy: [{ district_id: "asc" }, { year: "asc" }, {
month: "asc",
}],
});
const crimeCategories = await this.prisma.crime_categories
.findMany();
if (crimeCategories.length === 0) {
console.error(
"No crime categories found, please seed crime categories first",
);
return;
}
let totalIncidentsCreated = 0;
let skippedMonths = 0;
let processingStats = {
districts: new Set(),
years: new Set(),
totalMatched: 0,
totalMismatched: 0,
totalResolved: 0,
totalUnresolved: 0,
};
let yearlyStats = new Map<number, {
incidents: number;
resolved: number;
matched: number;
mismatched: number;
districts: Set<string>;
}>();
for (const crimeRecord of crimeRecords) {
if (crimeRecord.number_of_crime === 0) {
skippedMonths++;
continue;
}
const key =
`${crimeRecord.district_id}-${crimeRecord.month}-${crimeRecord.year}`;
const crimeMonthlyInfo = this.crimeMonthlyData.get(key);
if (
!crimeMonthlyInfo || crimeMonthlyInfo.number_of_crime <= 0
) {
skippedMonths++;
continue;
}
processingStats.districts.add(crimeRecord.district_id);
processingStats.years.add(crimeRecord.year);
const incidents = await this.createMockIncidentsForCrime(
crimeRecord,
crimeCategories,
);
const year = crimeRecord.year || 0;
if (!yearlyStats.has(year)) {
yearlyStats.set(year, {
incidents: 0,
resolved: 0,
matched: 0,
mismatched: 0,
districts: new Set(),
});
}
const yearStats = yearlyStats.get(year)!;
yearStats.incidents += incidents.length;
yearStats.districts.add(crimeRecord.district_id);
const resolvedCount = incidents.filter((inc) =>
inc.status === "resolved"
).length;
yearStats.resolved += resolvedCount;
if (crimeMonthlyInfo) {
const categoryCount =
Object.keys(crimeMonthlyInfo.categories).length;
yearStats.matched += categoryCount - 1;
yearStats.mismatched += 1;
}
totalIncidentsCreated += incidents.length;
}
for (const [year, stats] of Array.from(yearlyStats.entries())) {
console.log(`\n📊 ${year} Data Summary:`);
console.log(`├─ Total incidents created: ${stats.incidents}`);
console.log(`├─ Districts processed: ${stats.districts.size}`);
console.log(
`├─ Categories: ${stats.matched} matched, ${stats.mismatched} mismatched`,
);
console.log(`└─ Total resolved cases: ${stats.resolved}`);
}
await this.generateMock2025Incidents();
console.log("✅ Seeding selesai!");
} catch (error) {
console.error("❌ Error seeding crime incidents:", error);
throw error;
}
}
private generateDistributedPoints(
landArea: number,
numPoints: number,
districtId: string,
districtName: string,
): Array<{ latitude: number; longitude: number; radius: number }> {
const points = [];
const districtNameLower = districtName.toLowerCase();
const districtCenter = districtCenters.find(
(center) => center.kecamatan.toLowerCase() === districtNameLower,
);
if (!districtCenter) {
return [];
}
const centerLat = districtCenter.lat;
const centerLng = districtCenter.lng;
let scalingFactor = 0.3;
const effectiveLandArea = Math.max(landArea || 1, 1);
const radiusKm = Math.sqrt(effectiveLandArea) * scalingFactor;
const radiusDeg = radiusKm / 111;
for (let i = 0; i < numPoints; i++) {
const angle = Math.random() * 2 * Math.PI;
const distanceFactor = Math.pow(Math.random(), 1.5);
const distance = distanceFactor * radiusDeg;
const latitude = centerLat + distance * Math.cos(angle);
const longitude = centerLng +
distance * Math.sin(angle) /
Math.cos(centerLat * Math.PI / 180);
const pointRadius = distance * 111000;
points.push({
latitude,
longitude,
radius: pointRadius,
});
}
return points;
}
}
if (require.main === module) {
const testSeeder = async () => {
const prisma = new PrismaClient();
const seeder = new CrimeIncidentsByTypeSeeder(prisma);
try {
await seeder.run();
} catch (e) {
console.error("Error during seeding:", e);
process.exit(1);
} finally {
await prisma.$disconnect();
}
};
testSeeder();
}

View File

@ -9,6 +9,7 @@ import { createClient } from '../../app/_utils/supabase/client';
import * as fs from 'fs';
import * as path from 'path';
import csv from 'csv-parser';
import { CRegex } from '../../app/_utils/const/regex';
type ICreateLocations = {
id: string;
@ -24,7 +25,7 @@ type ICreateLocations = {
location: string;
};
export class CrimeIncidentsSeeder {
export class CrimeIncidentsByUnitSeeder {
private crimeMonthlyData: Map<
string,
{ number_of_crime: number; crime_cleared: number }
@ -187,7 +188,14 @@ private generateDistributedPoints(
await this.loadCrimeMonthlyData();
// Check if crime incidents already exist
const existingIncidents = await this.prisma.crime_incidents.findFirst();
const existingIncidents = await this.prisma.crime_incidents.findFirst({
where: {
crimes: {
source_type: "cbu"
}
}
});
if (existingIncidents) {
console.log('Crime incidents data already exists, skipping import.');
return;
@ -197,8 +205,8 @@ private generateDistributedPoints(
const crimeRecords = await this.prisma.crimes.findMany({
where: {
month: { not: null },
// Only process records with incidents (number_of_crime > 0)
number_of_crime: { gt: 0 },
source_type: "cbu"
},
orderBy: [{ district_id: 'asc' }, { year: 'asc' }, { month: 'asc' }],
});
@ -466,7 +474,7 @@ private generateDistributedPoints(
separator: '-',
uniquenessStrategy: 'counter',
},
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
CRegex.CR_YEAR_SEQUENCE
);
// Determine status based on crime_cleared
@ -582,7 +590,7 @@ private generateDistributedPoints(
if (require.main === module) {
const testSeeder = async () => {
const prisma = new PrismaClient();
const seeder = new CrimeIncidentsSeeder(prisma);
const seeder = new CrimeIncidentsByUnitSeeder(prisma);
try {
await seeder.run();
} catch (e) {

View File

@ -1,6 +1,8 @@
import {
PrismaClient,
crime_incidents,
crime_rates,
crimes,
events,
session_status,
users,
@ -9,6 +11,8 @@ import fs from 'fs';
import path from 'path';
import { parse } from 'csv-parse/sync';
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
import { CRegex } from '../../app/_utils/const/regex';
interface ICreateUser {
id: string;
@ -32,8 +36,6 @@ export class CrimesSeeder {
async run(): Promise<void> {
console.log('🌱 Seeding crimes data...');
// Clear existing data
try {
// Create test user
const user = await this.createUsers();
@ -48,15 +50,16 @@ export class CrimesSeeder {
// Create 5 sessions with completed status
await this.createSessions(user, events);
// Import monthly crime data
// Import original crime data with source_type = 'general'
await this.importMonthlyCrimeData();
// Import yearly crime data from CSV file
await this.importYearlyCrimeData();
// Import all-year crime summaries (2020-2024)
await this.importAllYearSummaries();
// Import new crime data by type with appropriate source_type
await this.importMonthlyCrimeDataByType();
await this.importYearlyCrimeDataByType();
await this.importSummaryByType();
console.log('✅ Crime seeding completed successfully.');
} catch (error) {
console.error('❌ Error seeding crimes:', error);
@ -65,7 +68,6 @@ export class CrimesSeeder {
}
private async createUsers() {
// Check if test users already exist
const existingUser = await this.prisma.users.findFirst({
where: { email: 'sigapcompany@gmail.com' },
});
@ -75,7 +77,6 @@ export class CrimesSeeder {
return existingUser;
}
// Get admin role ID
let roleId = await this.prisma.roles.findFirst({
where: { name: 'admin' },
});
@ -89,7 +90,6 @@ export class CrimesSeeder {
});
}
// Create test user directly with Prisma (no Supabase)
const newUser = await this.prisma.users.create({
data: {
email: 'sigapcompany@gmail.com',
@ -118,7 +118,6 @@ export class CrimesSeeder {
}
private async createEvents(user: ICreateUser) {
// Check if events already exist
const existingEvent = await this.prisma.events.findFirst({
where: {
user_id: user.id,
@ -142,7 +141,6 @@ export class CrimesSeeder {
}
private async createSessions(user: ICreateUser, events: events) {
// Check if sessions already exist
const existingSession = await this.prisma.sessions.findFirst();
if (existingSession) {
@ -161,7 +159,6 @@ export class CrimesSeeder {
return newSession;
}
// Helper function for chunked insertion (with default chunk size 500)
private async chunkedCreateMany(data: any[], chunkSize: number = 500) {
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
@ -174,33 +171,31 @@ export class CrimesSeeder {
private async importMonthlyCrimeData() {
console.log('Importing monthly crime data...');
// Check if crimes already exist
const existingCrimes = await this.prisma.crimes.findFirst();
const existingCrimes = await this.prisma.crimes.findFirst({
where: {
source_type: 'cbu',
},
});
if (existingCrimes) {
console.log('Crimes data already exists, skipping import.');
console.log('General crimes data already exists, skipping import.');
return;
}
// Read CSV file
const csvFilePath = path.resolve(
__dirname,
'../data/excels/crimes/crime_monthly.csv'
'../data/excels/crimes/crime_monthly_by_unit.csv'
);
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
// Parse CSV
const records = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
// Store unique district IDs to avoid duplicates
const processedDistricts = new Set<string>();
const crimesData: Array<Partial<crimes>> = [];
// Prepare batch data
const crimesData = [];
// Process records
for (const record of records) {
const crimeRate = record.level.toLowerCase() as crime_rates;
@ -216,7 +211,6 @@ export class CrimesSeeder {
}
const year = parseInt(record.year);
// Create a unique ID for monthly crime data
const crimeId = await generateIdWithDbCounter(
'crimes',
{
@ -230,11 +224,9 @@ export class CrimesSeeder {
separator: '-',
uniquenessStrategy: 'counter',
},
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
CRegex.CR_YEAR_SEQUENCE
);
// console.log('Creating crime ID:', crimeId);
crimesData.push({
id: crimeId,
district_id: record.district_id,
@ -243,14 +235,14 @@ export class CrimesSeeder {
month: parseInt(record.month_num),
year: parseInt(record.year),
number_of_crime: parseInt(record.number_of_crime),
crime_cleared: parseInt(record.crime_cleared) || 0,
score: parseFloat(record.score),
source_type: 'cbu',
});
// Keep track of unique districts for later creation of crime incidents
processedDistricts.add(record.district_id);
}
// Batch create all crimes in chunks
await this.chunkedCreateMany(crimesData);
console.log(`Imported ${records.length} monthly crime records.`);
@ -259,9 +251,11 @@ export class CrimesSeeder {
private async importYearlyCrimeData() {
console.log('Importing yearly crime data...');
// Check if yearly summaries already exist (records with null month)
const existingYearlySummary = await this.prisma.crimes.findFirst({
where: { month: null },
where: {
month: null,
source_type: 'cbu',
},
});
if (existingYearlySummary) {
@ -269,23 +263,19 @@ export class CrimesSeeder {
return;
}
// Read CSV file
const csvFilePath = path.resolve(
__dirname,
'../data/excels/crimes/crime_yearly.csv'
'../data/excels/crimes/crime_yearly_by_unit.csv'
);
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
// Parse CSV
const records = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
// Prepare batch data
const crimesData = [];
const crimesData: Array<Partial<crimes>> = [];
// Process records
for (const record of records) {
const crimeRate = record.level.toLowerCase() as crime_rates;
const year = parseInt(record.year);
@ -318,7 +308,7 @@ export class CrimesSeeder {
separator: '-',
uniquenessStrategy: 'counter',
},
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
CRegex.CR_YEAR_SEQUENCE
);
crimesData.push({
@ -329,11 +319,13 @@ export class CrimesSeeder {
month: null,
year: year,
number_of_crime: parseInt(record.number_of_crime),
crime_cleared: parseInt(record.crime_cleared) || 0,
avg_crime: parseFloat(record.avg_crime) || 0,
score: parseInt(record.score),
source_type: 'cbu',
});
}
// Batch create all yearly crimes in chunks
await this.chunkedCreateMany(crimesData);
console.log(`Imported ${records.length} yearly crime records.`);
@ -342,9 +334,12 @@ export class CrimesSeeder {
private async importAllYearSummaries() {
console.log('Importing all-year (2020-2024) crime summaries...');
// Check if all-year summaries already exist (records with null month and null year)
const existingAllYearSummaries = await this.prisma.crimes.findFirst({
where: { month: null, year: null },
where: {
month: null,
year: null,
source_type: 'cbu',
},
});
if (existingAllYearSummaries) {
@ -352,21 +347,18 @@ export class CrimesSeeder {
return;
}
// Read CSV file
const csvFilePath = path.resolve(
__dirname,
'../data/excels/crimes/district_summary_2020_2024.csv'
'../data/excels/crimes/crime_summary_by_unit.csv'
);
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
// Parse CSV
const records = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
// Prepare batch data
const crimesData = [];
const crimesData: Array<Partial<crimes>> = [];
for (const record of records) {
const crimeRate = record.level.toLowerCase() as crime_rates;
@ -387,7 +379,6 @@ export class CrimesSeeder {
continue;
}
// Create a unique ID for all-year summary data
const crimeId = await generateIdWithDbCounter(
'crimes',
{
@ -400,7 +391,7 @@ export class CrimesSeeder {
separator: '-',
uniquenessStrategy: 'counter',
},
/(\d{4})$/ // Pattern to extract the 4-digit counter at the end
CRegex.CR_SEQUENCE_END
);
crimesData.push({
@ -411,15 +402,253 @@ export class CrimesSeeder {
month: null,
year: null,
number_of_crime: parseInt(record.crime_total),
score: parseFloat(record.avg_score),
crime_cleared: parseInt(record.crime_cleared) || 0,
avg_crime: parseFloat(record.avg_crime) || 0,
score: parseFloat(record.score),
source_type: 'cbu',
});
}
// Batch create all all-year summaries in chunks
await this.chunkedCreateMany(crimesData);
console.log(`Imported ${records.length} all-year crime summaries.`);
}
private async importMonthlyCrimeDataByType() {
console.log('Importing monthly crime data by type...');
const existingCrimeByType = await this.prisma.crimes.findFirst({
where: {
source_type: 'cbt',
},
});
if (existingCrimeByType) {
console.log('Crime data by type already exists, skipping import.');
return;
}
const csvFilePath = path.resolve(
__dirname,
'../data/excels/crimes/crime_monthly_by_type.csv'
);
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
const records = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
const crimesData: Array<Partial<crimes>> = [];
for (const record of records) {
const crimeRate = record.level.toLowerCase() as crime_rates;
const city = await this.prisma.cities.findFirst({
where: {
name: 'Jember',
},
});
if (!city) {
console.error('City not found: Jember');
continue;
}
const year = parseInt(record.year);
const crimeId = await generateIdWithDbCounter(
'crimes',
{
prefix: 'CR',
segments: {
codes: [city.id],
sequentialDigits: 4,
year,
},
format: '{prefix}-{codes}-{sequence}-{year}',
separator: '-',
uniquenessStrategy: 'counter',
},
CRegex.CR_YEAR_SEQUENCE
);
crimesData.push({
id: crimeId,
district_id: record.district_id,
level: crimeRate,
method: record.method || 'kmeans',
month: parseInt(record.month_num),
year: parseInt(record.year),
number_of_crime: parseInt(record.number_of_crime),
crime_cleared: parseInt(record.crime_cleared) || 0,
score: parseFloat(record.score),
source_type: 'cbt',
});
}
await this.chunkedCreateMany(crimesData);
console.log(`Imported ${records.length} monthly crime by type records.`);
}
private async importYearlyCrimeDataByType() {
console.log('Importing yearly crime data by type...');
const existingYearlySummary = await this.prisma.crimes.findFirst({
where: {
month: null,
source_type: 'cbt',
},
});
if (existingYearlySummary) {
console.log('Yearly crime data by type already exists, skipping import.');
return;
}
const csvFilePath = path.resolve(
__dirname,
'../data/excels/crimes/crime_yearly_by_type.csv'
);
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
const records = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
const crimesData: Array<Partial<crimes>> = [];
for (const record of records) {
const crimeRate = record.level.toLowerCase() as crime_rates;
const year = parseInt(record.year);
const city = await this.prisma.cities.findFirst({
where: {
districts: {
some: {
id: record.district_id,
},
},
},
});
if (!city) {
console.error(`City not found for district ${record.district_id}`);
continue;
}
const crimeId = await generateIdWithDbCounter(
'crimes',
{
prefix: 'CR',
segments: {
codes: [city.id],
sequentialDigits: 4,
year,
},
format: '{prefix}-{codes}-{sequence}-{year}',
separator: '-',
uniquenessStrategy: 'counter',
},
CRegex.CR_YEAR_SEQUENCE
);
crimesData.push({
id: crimeId,
district_id: record.district_id,
level: crimeRate,
method: record.method || 'kmeans',
month: null,
year: year,
number_of_crime: parseInt(record.number_of_crime),
crime_cleared: parseInt(record.crime_cleared) || 0,
avg_crime: parseFloat(record.avg_crime) || 0,
score: parseInt(record.score),
source_type: 'cbt',
});
}
await this.chunkedCreateMany(crimesData);
console.log(`Imported ${records.length} yearly crime by type records.`);
}
private async importSummaryByType() {
console.log('Importing crime summary by type...');
const existingSummary = await this.prisma.crimes.findFirst({
where: {
source_type: 'cbt',
},
});
if (existingSummary) {
console.log('Crime summary by type already exists, skipping import.');
return;
}
const csvFilePath = path.resolve(
__dirname,
'../data/excels/crimes/crime_summary_by_type.csv'
);
const fileContent = fs.readFileSync(csvFilePath, { encoding: 'utf-8' });
const records = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
const crimesData: Array<Partial<crimes>> = [];
for (const record of records) {
const crimeRate = record.level.toLowerCase() as crime_rates;
const city = await this.prisma.cities.findFirst({
where: {
name: 'Jember',
},
});
if (!city) {
console.error('City not found: Jember');
continue;
}
const crimeId = await generateIdWithDbCounter(
'crimes',
{
prefix: 'CR',
segments: {
codes: [city.id],
sequentialDigits: 4,
},
format: '{prefix}-{codes}-{sequence}',
separator: '-',
uniquenessStrategy: 'counter',
},
CRegex.CR_SEQUENCE_END
);
crimesData.push({
id: crimeId,
district_id: record.district_id,
level: crimeRate,
method: 'kmeans',
month: null,
year: null,
number_of_crime: parseInt(record.crime_total),
crime_cleared: parseInt(record.crime_cleared) || 0,
avg_crime: parseFloat(record.avg_crime) || 0,
score: parseFloat(record.score),
source_type: 'cbt',
});
}
await this.chunkedCreateMany(crimesData);
console.log(`Imported ${records.length} crime summary by type records.`);
}
}
// This allows the file to be executed standalone for testing

View File

@ -31,6 +31,9 @@ export class DemographicsSeeder {
// Collect demographic data to be inserted in batch
const demographicsToInsert = [];
// Track 2024 data to duplicate for 2025
const data2024ByDistrict: Record<string, any> = {};
for (const row of data) {
const districtName = String(row['Kecamatan']).trim();
@ -51,17 +54,38 @@ export class DemographicsSeeder {
const populationDensity =
districtLandArea > 0 ? population / districtLandArea : 0;
demographicsToInsert.push({
const demographicRecord = {
district_id: district.id,
year,
population,
population_density: populationDensity,
number_of_unemployed: unemployed,
});
};
demographicsToInsert.push(demographicRecord);
// Store 2024 data for later duplication
if (year === 2024) {
data2024ByDistrict[district.id] = demographicRecord;
}
counter++;
}
// Create 2025 data based on 2024 data
for (const districtId in data2024ByDistrict) {
const record2024 = data2024ByDistrict[districtId];
const record2025 = {
...record2024,
year: 2025 // Change year to 2025
};
demographicsToInsert.push(record2025);
counter++;
}
console.log(`✅ Added ${Object.keys(data2024ByDistrict).length} demographic records for 2025 based on 2024 data`);
// Insert all demographic data at once
await this.prisma.demographics.createMany({

View File

@ -57,12 +57,16 @@ export class GeoJSONSeeder {
jsonData.forEach((row: any) => {
const districtName = row['Kecamatan'];
if (districtName) {
// Get 2024 value to use for 2025
const value2024 = this.parseAreaValue(row['2024']);
this.areaData[districtName] = {
'2020': this.parseAreaValue(row['2020']),
'2021': this.parseAreaValue(row['2021']),
'2022': this.parseAreaValue(row['2022']),
'2023': this.parseAreaValue(row['2023']),
'2024': this.parseAreaValue(row['2024']),
'2025': value2024, // Use the same value as 2024
};
}
});
@ -196,7 +200,7 @@ export class GeoJSONSeeder {
const districtsToCreate = [];
const geographicsToCreate = [];
const addressPromises = [];
const years = [2020, 2021, 2022, 2023, 2024];
const years = [2020, 2021, 2022, 2023, 2024, 2025]; // Added 2025
// 2. Process all districts first to prepare data
for (let i = 0; i < districtGeoJson.features.length; i++) {

View File

@ -114,7 +114,6 @@ export class UnitSeeder {
});
unitsToInsert.push({
district_id: patrangDistrict.id,
city_id: city.id,
code_unit: polresId,
name: `Polres ${city.name}`,

View File

@ -0,0 +1,157 @@
-- Function to calculate distance from location to its district's assigned police unit
CREATE OR REPLACE FUNCTION gis.calculate_distance_to_district_unit()
RETURNS TRIGGER AS $$
BEGIN
-- Calculate the distance to the unit assigned to this district
SELECT ST_Distance(
NEW.location::geography,
u.location::geography
) / 1000 -- Convert to kilometers
INTO NEW.distance_to_unit
FROM units u
WHERE u.district_id = NEW.district_id;
-- If no unit found for this district, set distance_to_unit to NULL
-- This indicates that the district doesn't have an assigned unit
IF NEW.distance_to_unit IS NULL THEN
RAISE NOTICE 'No assigned unit found for district id %', NEW.district_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger to automatically calculate distance when a location is inserted or updated
CREATE TRIGGER location_distance_to_unit_trigger
BEFORE INSERT OR UPDATE ON locations
FOR EACH ROW
EXECUTE FUNCTION gis.calculate_distance_to_district_unit();
-- Comment explaining what this migration does
COMMENT ON FUNCTION gis.calculate_distance_to_district_unit() IS 'Calculates the distance from a location to its district''s assigned police unit and populates the distance_to_unit field';
COMMENT ON TRIGGER location_distance_to_unit_trigger ON locations IS 'Automatically calculates distance to district''s assigned police unit when a location is inserted or updated';
-- Function to automatically create resource entries when new tables are created
-- and then create CRUD permissions for all admin users
-- Modified to work without event triggers
CREATE OR REPLACE FUNCTION gis.create_resource_and_permissions_for_table(table_name text)
RETURNS void AS $$
DECLARE
resource_id uuid;
admin_role_id uuid;
BEGIN
-- Get the admin role ID
SELECT id INTO admin_role_id FROM roles WHERE name = 'admin';
-- If admin role doesn't exist, log a notice and exit
IF admin_role_id IS NULL THEN
RAISE NOTICE 'Admin role not found. No permissions created.';
RETURN;
END IF;
-- Skip system tables and migration tables
IF table_name ~ '^(pg_|_|migrations)' THEN
RETURN;
END IF;
-- Check if resource already exists for this table
IF NOT EXISTS (SELECT 1 FROM resources WHERE name = table_name) THEN
-- Create new resource entry
INSERT INTO resources (
id,
name,
type,
description,
instance_role,
relations,
attributes,
created_at,
updated_at
) VALUES (
gen_random_uuid(),
table_name,
'table',
'Auto-generated resource for table ' || table_name,
NULL,
NULL,
'{"auto_generated": true}'::jsonb,
NOW(),
NOW()
)
RETURNING id INTO resource_id;
RAISE NOTICE 'Created new resource for table %', table_name;
-- Create CRUD permissions for admin role
-- Create permission
INSERT INTO permissions (id, action, resource_id, role_id, created_at, updated_at)
VALUES (
gen_random_uuid(),
'create',
resource_id,
admin_role_id,
NOW(),
NOW()
);
-- Read permission
INSERT INTO permissions (id, action, resource_id, role_id, created_at, updated_at)
VALUES (
gen_random_uuid(),
'read',
resource_id,
admin_role_id,
NOW(),
NOW()
);
-- Update permission
INSERT INTO permissions (id, action, resource_id, role_id, created_at, updated_at)
VALUES (
gen_random_uuid(),
'update',
resource_id,
admin_role_id,
NOW(),
NOW()
);
-- Delete permission
INSERT INTO permissions (id, action, resource_id, role_id, created_at, updated_at)
VALUES (
gen_random_uuid(),
'delete',
resource_id,
admin_role_id,
NOW(),
NOW()
);
RAISE NOTICE 'Created CRUD permissions for admin role on resource %', table_name;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Function to backfill resources and permissions for existing tables
CREATE OR REPLACE FUNCTION gis.backfill_resources_and_permissions()
RETURNS void AS $$
DECLARE
table_record record;
BEGIN
-- For each existing table in the public schema
FOR table_record IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
LOOP
-- Call the function to create resource and permissions for each table
PERFORM gis.create_resource_and_permissions_for_table(table_record.table_name);
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- Comments explaining the functions
COMMENT ON FUNCTION gis.create_resource_and_permissions_for_table(text) IS 'Creates resource entry and admin CRUD permissions for the specified table';
COMMENT ON FUNCTION gis.backfill_resources_and_permissions() IS 'Function to backfill resources and permissions for existing tables';