diff --git a/.gitignore b/.gitignore index ea6a851..fc38adb 100755 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,20 @@ database.ini settings.json check.py -step8-priv.py -__pycache__/ data Barbora notebooks first scrape tries +personal_finance/fin/analysis +personal_finance/fin/analysis/Manually polished +personal_finance/income +personal_finance/OG-CSV +personal_finance/spending +personal_notes/clickup_extract/old_output +personal_notes/clickup_extract/source data +personal_notes/discord_extract/categories +src/personal_notes/discord_extract/.env.env +.env +Data-Engineering +__pycache__ + diff --git a/src/Archives/automation/plan.txt b/src/Archives/automation/plan.txt new file mode 100755 index 0000000..e69de29 diff --git a/src/LoL-support/Database/__pycache__/config.cpython-311.pyc b/src/LoL-support/Database/__pycache__/config.cpython-311.pyc new file mode 100755 index 0000000..73d7681 Binary files /dev/null and b/src/LoL-support/Database/__pycache__/config.cpython-311.pyc differ diff --git a/src/LoL-support/Database/__pycache__/config.cpython-312.pyc b/src/LoL-support/Database/__pycache__/config.cpython-312.pyc new file mode 100755 index 0000000..27a4159 Binary files /dev/null and b/src/LoL-support/Database/__pycache__/config.cpython-312.pyc differ diff --git a/src/LoL-support/Database/config.py b/src/LoL-support/Database/config.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/Database/main.py b/src/LoL-support/Database/main.py old mode 100644 new mode 100755 index 2d79050..6654d6f --- a/src/LoL-support/Database/main.py +++ b/src/LoL-support/Database/main.py @@ -2,15 +2,6 @@ from config import config # Connect to the PostgreSQL database -"""conn = psycopg2.connect( - host="127.0.0.1", - port="5432", - dbname="league", - user="postgres", - password="0809" -) -""" - def connect(): connection = None try: diff --git a/src/LoL-support/Leage_workspace.code-workspace b/src/LoL-support/Leage_workspace.code-workspace new file mode 100755 index 0000000..407c760 --- /dev/null +++ b/src/LoL-support/Leage_workspace.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "../.." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/LoL-support/archive/adc_names.json b/src/LoL-support/archive/adc_names.json old mode 100644 new mode 100755 diff --git a/src/LoL-support/archive/jungler_names.json b/src/LoL-support/archive/jungler_names.json old mode 100644 new mode 100755 diff --git a/src/LoL-support/archive/mid_names.json b/src/LoL-support/archive/mid_names.json old mode 100644 new mode 100755 diff --git a/src/LoL-support/archive/step5-regex.py b/src/LoL-support/archive/step5-regex.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/champion_names_test.json b/src/LoL-support/champion_names_test.json old mode 100644 new mode 100755 diff --git a/src/LoL-support/counters_processed.json b/src/LoL-support/counters_processed.json old mode 100644 new mode 100755 index 43dbd0d..ae49a71 --- a/src/LoL-support/counters_processed.json +++ b/src/LoL-support/counters_processed.json @@ -1,166 +1,86 @@ [ - { - "champion_name": "Taric", - "counters": "Soraka(55.26%), Rell(52.91%), Vel'Koz(52.75%), Janna(50.88%), Zilean(50.55%), Rakan(49.62%), Maokai(49.58%), Nami(49.42%), Xerath(49.28%), Senna(49.24%)" - }, - { - "champion_name": "Janna", - "counters": "Senna(50.79%), Sona(50.1%), Soraka(50.01%), Seraphine(49.87%), Milio(49.71%), Neeko(49.67%), Rell(49.5%), Bard(49.44%), Rakan(49.42%), Blitzcrank(49.4%)" - }, { "champion_name": "Maokai", - "counters": "Shaco(58.18%), Braum(54.8%), Sona(53.99%), Janna(52.72%), Renata Glasc(52.39%), Zyra(51.46%), Alistar(51.45%), Soraka(50.8%), Taric(50.42%), Neeko(50.29%)" - }, - { - "champion_name": "Bard", - "counters": "Taric(52.2%), Janna(50.56%), Maokai(50.28%), Zilean(49.95%), Senna(49.94%), Blitzcrank(49.89%), Renata Glasc(49.52%), Soraka(49.45%), Rell(49.3%), Pyke(49.06%)" - }, - { - "champion_name": "Rakan", - "counters": "Senna(52.31%), Bard(51.42%), Soraka(51.41%), Neeko(51.37%), Renata Glasc(51.09%), Sona(50.93%), Maokai(50.87%), Rell(50.69%), Zilean(50.64%), Janna(50.58%)" - }, - { - "champion_name": "Sona", - "counters": "Taric(56.83%), Leona(55.04%), Seraphine(53.49%), Blitzcrank(52.56%), Senna(51.71%), Bard(51.4%), Nami(50.59%), Milio(50.43%), Zilean(50.26%), Pyke(50.19%)" - }, - { - "champion_name": "Senna", - "counters": "Pyke(52.31%), Blitzcrank(52.25%), Xerath(51.66%), Maokai(51.62%), Zyra(51.1%), Taric(50.76%), Leona(50.22%), Bard(50.06%), Lux(49.9%), Rell(49.8%)" + "counters": "Taric(50.48%), Alistar(49.83%), Braum(49.49%), Leona(49.02%), Janna(48.98%), Renata Glasc(48.48%), Rell(47.97%), Shaco(47.18%), Camille(47.04%), Zyra(47.02%)" }, { - "champion_name": "Rell", - "counters": "Poppy(53.79%), Seraphine(53.57%), Soraka(52.57%), Zilean(52.54%), Alistar(52.45%), Shaco(51.5%), Maokai(50.89%), Sona(50.75%), Bard(50.7%), Janna(50.5%)" - }, - { - "champion_name": "Neeko", - "counters": "Vel'Koz(60.45%), Sona(53.26%), Bard(52.61%), Brand(52.38%), Taric(52.17%), Alistar(51%), Shaco(50.68%), Senna(50.42%), Renata Glasc(50.38%), Morgana(50.35%)" + "champion_name": "Janna", + "counters": "Camille(53.25%), Sona(51.89%), Blitzcrank(51.2%), Maokai(51.02%), Braum(49.67%), Pyke(49.38%), Renata Glasc(49.3%), Senna(49.25%), Ashe(49.07%), Nami(48.64%)" }, { - "champion_name": "Soraka", - "counters": "Blitzcrank(54.17%), Neeko(51.31%), Sona(51.25%), Zyra(51.13%), Vel'Koz(51.05%), Bard(50.55%), Senna(50.38%), Yuumi(50.06%), Janna(49.99%), Pyke(49.98%)" + "champion_name": "Taric", + "counters": "Swain(57.63%), Sona(57.32%), Janna(54.88%), Nami(54.27%), Lux(53.54%), Seraphine(53.45%), Bard(52.97%), Soraka(52.32%), Thresh(52.21%), Braum(51.67%)" }, { "champion_name": "Blitzcrank", - "counters": "Taric(56.85%), Rakan(52.88%), Leona(52.47%), Braum(52.28%), Renata Glasc(51.05%), Maokai(51.04%), Zyra(50.98%), Rell(50.78%), Alistar(50.73%), Zilean(50.7%)" + "counters": "Pantheon(54.85%), Leona(54.08%), Maokai(53.66%), Braum(53.61%), Rakan(53.26%), Taric(53.13%), Alistar(52.33%), Shaco(51.5%), Rell(50.9%), Zilean(50.57%)" }, { - "champion_name": "Zyra", - "counters": "Sona(53.56%), Pyke(53.25%), Heimerdinger(53.11%), Bard(53.07%), Leona(51.91%), Janna(51.88%), Shaco(51.56%), Taric(51.43%), Rakan(50.98%), Nautilus(50.75%)" + "champion_name": "Camille", + "counters": "Taric(58.76%), Renata Glasc(58.12%), Braum(57.32%), Leona(54.48%), Brand(54.2%), Bard(53.76%), Zac(53.03%), Maokai(52.96%), Tahm Kench(52.94%), Zilean(52.78%)" }, { - "champion_name": "Pyke", - "counters": "Rakan(53.38%), Maokai(52.78%), Neeko(52.1%), Rell(52.08%), Taric(51.82%), Janna(51.79%), Blitzcrank(51.44%), Alistar(50.94%), Bard(50.94%), Thresh(50.49%)" + "champion_name": "Zac", + "counters": "Pantheon(61.36%), Taric(58.82%), Zilean(58.49%), Rell(56.82%), Bard(56.55%), Morgana(55.93%), Janna(54.44%), Maokai(53.44%), Milio(53.25%), Thresh(53.18%)" }, { - "champion_name": "Zilean", - "counters": "Janna(53.27%), Shaco(53.02%), Senna(51.99%), Neeko(51.67%), Maokai(51.41%), Xerath(51.07%), Milio(50.67%), Sylas(50.34%), Soraka(50.28%), Vel'Koz(50.23%)" + "champion_name": "Bard", + "counters": "Maokai(56.1%), Renata Glasc(53.77%), Janna(53.45%), Pyke(51.79%), Morgana(51.04%), Nami(50.92%), Pantheon(50.72%), Blitzcrank(50.43%), Brand(50.34%), Senna(50.21%)" }, { - "champion_name": "Braum", - "counters": "Zilean(55.95%), Soraka(54.2%), Bard(53.76%), Rell(53.74%), Taric(53.44%), Sona(53.2%), Senna(53.06%), Rakan(52.88%), Janna(52.49%), Renata Glasc(52.31%)" + "champion_name": "Sona", + "counters": "Zilean(61.68%), Rell(59.62%), Leona(57.21%), Neeko(55.71%), Thresh(55.03%), Maokai(54.7%), Bard(53.59%), Braum(51.81%), Blitzcrank(51.79%), Seraphine(51.2%)" }, { - "champion_name": "Vel'Koz", - "counters": "Pyke(55.14%), Zyra(54.76%), Seraphine(54.15%), Blitzcrank(53.89%), Janna(53.62%), Bard(53.22%), Sona(52.97%), Nami(52.1%), Maokai(51.85%), Renata Glasc(51.64%)" + "champion_name": "Zyra", + "counters": "Taric(54.73%), Leona(54.05%), Sona(53.31%), Maokai(52.98%), Renata Glasc(52.84%), Pyke(52.6%), Zac(52.59%), Janna(52.42%), Zilean(51.88%), Yuumi(51.63%)" }, { - "champion_name": "Thresh", - "counters": "Taric(53.39%), Janna(52.43%), Brand(52.12%), Rakan(52.02%), Neeko(51.75%), Sona(51.47%), Maokai(51.34%), Vel'Koz(51.27%), Bard(51.24%), Renata Glasc(51.24%)" + "champion_name": "Nautilus", + "counters": "Taric(60.99%), Leona(57.54%), Braum(56.73%), Renata Glasc(55.96%), Alistar(55.65%), Rell(55.38%), Camille(54.74%), Pantheon(54.53%), Swain(54.35%), Maokai(53.72%)" }, { "champion_name": "Milio", - "counters": "Taric(54.79%), Blitzcrank(53.66%), Braum(53.22%), Vel'Koz(53.08%), Senna(52.78%), Bard(52.75%), Neeko(52.17%), Rakan(51.91%), Soraka(51.52%), Thresh(51.33%)" - }, - { - "champion_name": "Alistar", - "counters": "Sona(54.51%), Janna(54.04%), Swain(53.24%), Vel'Koz(52.78%), Senna(52.64%), Zilean(52.39%), Soraka(52.22%), Seraphine(51.97%), Xerath(51.86%), Milio(51.85%)" - }, - { - "champion_name": "Zac", - "counters": "Poppy(67.39%), Maokai(57.14%), Neeko(56.72%), Swain(56%), Vel'Koz(54.1%), Taric(53.85%), Alistar(53.74%), Ashe(53.49%), Rakan(53.42%), Zilean(53.26%)" + "counters": "Blitzcrank(56.32%), Braum(55.45%), Maokai(54.37%), Camille(53.86%), Janna(53.43%), Thresh(53.37%), Zilean(53.04%), Tahm Kench(53.04%), Vel'Koz(52.98%), Leona(52.96%)" }, { - "champion_name": "Leona", - "counters": "Taric(56.7%), Janna(54.1%), Bard(54.03%), Shaco(53.67%), Maokai(53.55%), Rell(53.22%), Soraka(53.11%), Lux(53.09%), Morgana(53.02%), Braum(51.72%)" + "champion_name": "Nami", + "counters": "Morgana(56.08%), Blitzcrank(55.61%), Maokai(55.57%), Vel'Koz(54.47%), Rakan(54.44%), Neeko(54.21%), Rell(53.9%), Nautilus(53.32%), Thresh(53.28%), Senna(53.17%)" }, { "champion_name": "Xerath", - "counters": "Sylas(55%), Maokai(53.98%), Sona(53.77%), Janna(53.15%), Pyke(52.84%), Soraka(52.33%), Rakan(52.02%), Leona(51.76%), Zyra(51.72%), Nami(51.6%)" + "counters": "Blitzcrank(58.63%), Neeko(56.43%), Sona(56.36%), Maokai(56.26%), Pyke(56.15%), Leona(56.02%), Braum(55.06%), Rakan(54.83%), Nautilus(54.65%), Janna(53.86%)" }, { - "champion_name": "Nami", - "counters": "Maokai(56.37%), Neeko(53.14%), Blitzcrank(53.03%), Rakan(52.58%), Zyra(52.29%), Soraka(51.92%), Milio(51.67%), Pyke(51.56%), Janna(51.44%), Bard(51.33%)" + "champion_name": "Swain", + "counters": "Zac(58.97%), Sona(58.67%), Neeko(57.41%), Nami(57.14%), Milio(56.44%), Pantheon(55.95%), Zyra(55.49%), Morgana(55.24%), Leona(55%), Camille(54.79%)" }, { - "champion_name": "Shaco", - "counters": "Sona(60.95%), Swain(60.22%), Taric(59.68%), Heimerdinger(59.02%), Zac(56.14%), Janna(55.96%), Milio(54.49%), Pyke(54.23%), Rakan(53.2%), Bard(52.59%)" + "champion_name": "Lulu", + "counters": "Vel'Koz(57.35%), Blitzcrank(56.23%), Zilean(55.78%), Tahm Kench(55.43%), Renata Glasc(54.86%), Rell(54.7%), Janna(54.6%), Maokai(54.51%), Braum(53.79%), Ashe(53.65%)" }, { - "champion_name": "Heimerdinger", - "counters": "Maokai(61.64%), Neeko(56.82%), Seraphine(55.95%), Xerath(54.86%), Bard(54.55%), Leona(54.4%), Rell(53.15%), Rakan(52.67%), Lulu(52.56%), Janna(52.56%)" + "champion_name": "Ashe", + "counters": "Camille(58.72%), Maokai(57.34%), Pyke(56.14%), Blitzcrank(55.88%), Nautilus(55.46%), Taric(55.41%), Vel'Koz(54.55%), Zyra(54.13%), Soraka(53.87%), Xerath(53.83%)" }, { "champion_name": "Sylas", - "counters": "Sona(60%), Janna(57.91%), Vel'Koz(57.75%), Braum(57.52%), Morgana(57.5%), Taric(55.56%), Rell(54.55%), Rakan(53.89%), Blitzcrank(53.74%), Thresh(53.64%)" - }, - { - "champion_name": "Tahm Kench", - "counters": "Taric(62.22%), Braum(61.11%), Bard(57.4%), Zyra(57.27%), Pyke(55.38%), Maokai(54.9%), Brand(54.67%), Lulu(54.55%), Renata Glasc(54.17%), Rakan(54.12%)" - }, - { - "champion_name": "Amumu", - "counters": "Maokai(66.07%), Zyra(61.68%), Rell(58.9%), Rakan(58.55%), Janna(58.5%), Neeko(57.14%), Taric(56.1%), Braum(55.41%), Bard(54%), Leona(53.91%)" - }, - { - "champion_name": "Twitch", - "counters": "Zilean(61.97%), Taric(60.47%), Leona(57.92%), Soraka(57.87%), Shaco(57.38%), Nami(57.14%), Rakan(56.34%), Sylas(55.56%), Brand(55.26%), Pyke(54.65%)" - }, - { - "champion_name": "Nautilus", - "counters": "Taric(55.98%), Rell(55.66%), Renata Glasc(55.13%), Sylas(55.09%), Braum(54.08%), Rakan(54.06%), Vel'Koz(53.45%), Swain(53.09%), Neeko(52.97%), Alistar(52.87%)" + "counters": "Taric(70%), Shaco(70%), Pantheon(64.86%), Neeko(63.16%), Thresh(58.58%), Lulu(58.52%), Morgana(58.06%), Janna(57.66%), Leona(57.58%), Bard(57.45%)" }, { - "champion_name": "Ashe", - "counters": "Pyke(56.98%), Sylas(54.95%), Blitzcrank(54.83%), Heimerdinger(54.8%), Rell(54.67%), Nautilus(54.35%), Bard(53.44%), Milio(53.1%), Rakan(52.69%), Brand(52.5%)" + "champion_name": "Neeko", + "counters": "Zilean(60.66%), Taric(58.7%), Zac(57.78%), Leona(57.76%), Pantheon(57.14%), Janna(56.74%), Blitzcrank(56.38%), Bard(55.85%), Camille(55.41%), Rell(55.19%)" }, { "champion_name": "Lux", - "counters": "Blitzcrank(56.57%), Maokai(54.94%), Zyra(54.66%), Shaco(54.45%), Vel'Koz(54.1%), Sylas(53.87%), Zilean(53.51%), Pyke(53.5%), Xerath(52.75%), Nami(52.72%)" - }, - { - "champion_name": "Brand", - "counters": "Sona(58.11%), Maokai(57.24%), Xerath(57.03%), Janna(56.31%), Pyke(56.01%), Zyra(55.13%), Leona(54.96%), Zilean(54.92%), Soraka(54.73%), Senna(54.19%)" - }, - { - "champion_name": "Seraphine", - "counters": "Sylas(58.33%), Maokai(57.24%), Zilean(56.09%), Bard(55.97%), Pyke(54.69%), Soraka(53.72%), Blitzcrank(53.61%), Yuumi(53.6%), Milio(52.99%), Thresh(52.66%)" - }, - { - "champion_name": "Lulu", - "counters": "Taric(56.84%), Zilean(54.43%), Neeko(54.04%), Senna(54.04%), Sona(53.78%), Zyra(53.7%), Maokai(53.46%), Blitzcrank(52.96%), Bard(52.89%), Thresh(52.88%)" - }, - { - "champion_name": "Morgana", - "counters": "Zyra(56.73%), Senna(56.35%), Rell(56.29%), Janna(55.6%), Milio(55.2%), Vel'Koz(55.08%), Nami(54.67%), Taric(54.31%), Karma(54.24%), Maokai(53.2%)" - }, - { - "champion_name": "Karma", - "counters": "Pyke(57.3%), Maokai(55.33%), Zyra(54.97%), Blitzcrank(54.92%), Rakan(54.52%), Janna(54.36%), Sona(54.33%), Nautilus(54.21%), Soraka(53.61%), Neeko(52.83%)" - }, - { - "champion_name": "Swain", - "counters": "Zilean(57.77%), Sona(57.32%), Vel'Koz(57.25%), Janna(56.41%), Brand(55.87%), Yuumi(55.78%), Zyra(55.78%), Xerath(55.48%), Soraka(55.34%), Senna(55.32%)" - }, - { - "champion_name": "Pantheon", - "counters": "Neeko(59.74%), Maokai(58.43%), Karma(57.22%), Bard(57.14%), Swain(56.25%), Rakan(55.92%), Brand(55.83%), Soraka(55.74%), Sona(55.56%), Janna(55.42%)" + "counters": "Camille(62.93%), Pantheon(60%), Pyke(58.92%), Bard(57.24%), Shaco(56.3%), Blitzcrank(55.42%), Maokai(55.39%), Leona(55.08%), Alistar(54.69%), Zilean(54.61%)" }, { "champion_name": "Yuumi", - "counters": "Maokai(61.28%), Rell(59.85%), Taric(57.47%), Alistar(57.31%), Nautilus(56.64%), Rakan(55.78%), Pyke(55.71%), Leona(55.63%), Sylas(55.27%), Thresh(55.23%)" + "counters": "Rell(62.65%), Braum(58.82%), Blitzcrank(58.05%), Leona(57.77%), Pyke(57.61%), Taric(57.46%), Swain(56.85%), Rakan(56.72%), Maokai(56.12%), Janna(55.85%)" }, { "champion_name": "Hwei", - "counters": "Taric(66.91%), Sona(66.55%), Blitzcrank(65.66%), Janna(65.3%), Brand(65.18%), Pyke(63.85%), Xerath(63.02%), Vel'Koz(62.71%), Morgana(62.31%), Milio(62.01%)" + "counters": "Camille(63.76%), Pyke(60.47%), Maokai(58.72%), Xerath(58.7%), Rakan(58.47%), Taric(58.43%), Swain(58.1%), Zilean(57.69%), Vel'Koz(57.69%), Janna(57.32%)" } ] \ No newline at end of file diff --git a/src/LoL-support/data.json b/src/LoL-support/data.json old mode 100644 new mode 100755 index 78e746e..8cdf817 --- a/src/LoL-support/data.json +++ b/src/LoL-support/data.json @@ -1,46 +1,46 @@ { "div_elements": [ - "
1
\"supp\"
A
52.89%
1.4%
0.2%
14,268
", - "
2
\"supp\"
S+
52.13%
9.6%
4.5%
100,593
", - "
3
\"supp\"
A
51.99%
1.6%
0.6%
16,964
", - "
4
\"supp\"
S
51.82%
6.1%
0.9%
63,276
", - "
5
\"supp\"
S+
51.78%
12.8%
7.9%
133,770
", - "
6
\"supp\"
A
51.38%
2.4%
0.1%
25,396
", - "
7
\"supp\"
S+
51.36%
11.0%
10.8%
114,521
", - "
8
\"supp\"
S
51.35%
4.9%
4.3%
51,691
", - "
9
\"supp\"
A
51.22%
1.9%
5.4%
19,946
", - "
10
\"supp\"
S
51.19%
5.5%
2.1%
56,990
", - "
11
\"supp\"
S+
51.19%
8.8%
34.3%
92,387
", - "
12
\"supp\"
S
51.05%
3.8%
2.9%
39,963
", - "
13
\"supp\"
S+
51.02%
7.6%
16.6%
79,272
", - "
14
\"supp\"
A
51.02%
2.5%
0.7%
26,487
", - "
15
\"supp\"
A
50.60%
3.6%
0.7%
37,859
", - "
16
\"supp\"
A
50.54%
5.0%
2.0%
52,551
", - "
17
\"supp\"
B
50.24%
1.6%
0.2%
16,351
", - "
18
\"supp\"
S
50.23%
13.3%
5.1%
138,739
", - "
19
\"supp\"
A
50.22%
6.7%
2.3%
69,909
", - "
20
\"supp\"
A
50.19%
5.8%
2.8%
60,227
", - "
21
\"supp\"
B
50.16%
0.7%
2.2%
7,320
", - "
22
\"supp\"
A
50.15%
4.8%
1.8%
50,417
", - "
23
\"supp\"
A
49.97%
4.8%
8.9%
50,300
", - "
24
\"supp\"
B
49.93%
6.5%
0.2%
68,410
", - "
25
\"supp\"
B
49.83%
1.1%
10.7%
11,342
", - "
26
\"supp\"
B
49.66%
1.0%
1.1%
10,869
", - "
27
\"supp\"
B
49.54%
1.0%
20.0%
10,213
", - "
28
\"supp\"
B
49.40%
0.6%
0.5%
6,065
", - "
29
\"supp\"
B
49.35%
0.6%
0.4%
6,207
", - "
30
\"supp\"
B
49.26%
0.8%
4.5%
7,940
", - "
31
\"supp\"
D
49.09%
10.9%
16.7%
114,188
", - "
32
\"supp\"
D
48.74%
5.7%
9.0%
59,273
", - "
33
\"supp\"
D
48.70%
5.1%
2.5%
53,737
", - "
34
\"supp\"
B
48.62%
2.0%
4.7%
21,261
", - "
35
\"supp\"
B
48.60%
2.0%
0.4%
20,739
", - "
36
\"supp\"
D
48.58%
7.4%
4.4%
76,847
", - "
37
\"supp\"
D
48.54%
3.2%
11.9%
33,911
", - "
38
\"supp\"
D
48.09%
7.6%
2.1%
79,566
", - "
39
\"supp\"
C
47.52%
1.6%
0.7%
16,361
", - "
40
\"supp\"
B
47.01%
1.0%
2.4%
10,881
", - "
41
\"supp\"
D
46.73%
4.9%
5.4%
51,482
", - "
42
\"supp\"
D
39.39%
2.1%
29.4%
22,098
" + "
1
\"supp\"
S+
53.96%
12.7%
29.6%
69,959
", + "
2
\"supp\"
S+
52.51%
10.7%
5.9%
58,717
", + "
3
\"supp\"
A
52.05%
1.5%
0.2%
8,252
", + "
4
\"supp\"
A
52.00%
3.8%
0.5%
20,759
", + "
5
\"supp\"
S+
51.77%
8.1%
23.9%
44,423
", + "
6
\"supp\"
A
51.55%
2.3%
1.8%
12,418
", + "
7
\"supp\"
A
51.54%
4.4%
1.3%
24,495
", + "
8
\"supp\"
A
51.38%
4.1%
1.3%
22,625
", + "
9
\"supp\"
S
51.35%
7.7%
1.6%
42,551
", + "
10
\"supp\"
A
51.21%
2.8%
0.5%
15,658
", + "
11
\"supp\"
S+
51.20%
8.5%
18.2%
46,810
", + "
12
\"supp\"
A
50.84%
1.0%
5.3%
5,315
", + "
13
\"supp\"
A
50.61%
4.7%
1.7%
26,103
", + "
14
\"supp\"
A
50.56%
5.7%
2.5%
31,615
", + "
15
\"supp\"
A
50.50%
5.4%
1.8%
29,924
", + "
16
\"supp\"
A
50.48%
2.3%
0.1%
12,678
", + "
17
\"supp\"
A
50.42%
9.4%
2.5%
51,858
", + "
18
\"supp\"
A
50.41%
2.4%
0.4%
13,034
", + "
19
\"supp\"
A
50.17%
4.3%
3.9%
23,848
", + "
20
\"supp\"
B
50.13%
1.4%
0.2%
7,588
", + "
21
\"supp\"
A
49.97%
11.0%
18.8%
60,568
", + "
22
\"supp\"
B
49.89%
2.6%
10.8%
14,365
", + "
23
\"supp\"
B
49.88%
1.8%
1.8%
10,119
", + "
24
\"supp\"
B
49.33%
1.1%
0.3%
5,907
", + "
25
\"supp\"
D
49.05%
11.0%
10.6%
60,829
", + "
26
\"supp\"
D
49.02%
8.1%
6.1%
44,357
", + "
27
\"supp\"
C
48.81%
5.5%
0.2%
30,031
", + "
28
\"supp\"
B
48.62%
2.7%
4.0%
14,972
", + "
29
\"supp\"
B
48.57%
1.9%
7.8%
10,649
", + "
30
\"supp\"
B
48.50%
1.4%
0.4%
7,712
", + "
31
\"supp\"
D
48.43%
7.9%
3.8%
43,741
", + "
32
\"supp\"
D
48.05%
2.8%
9.4%
15,441
", + "
33
\"supp\"
B
47.94%
0.5%
0.4%
2,862
", + "
34
\"supp\"
D
47.88%
4.8%
4.1%
26,547
", + "
35
\"supp\"
B
47.86%
0.6%
6.6%
3,544
", + "
36
\"supp\"
B
47.80%
1.6%
2.5%
8,779
", + "
37
\"supp\"
D
47.79%
9.6%
24.8%
52,734
", + "
38
\"supp\"
C
47.77%
3.9%
1.4%
21,235
", + "
39
\"supp\"
B
47.70%
1.7%
1.7%
9,159
", + "
40
\"supp\"
D
47.02%
4.4%
4.0%
24,119
", + "
41
\"supp\"
B
46.82%
0.5%
3.1%
2,958
", + "
42
\"supp\"
D
45.48%
2.9%
18.5%
16,230
" ] } \ No newline at end of file diff --git a/src/LoL-support/final_data.json b/src/LoL-support/final_data.json old mode 100644 new mode 100755 index 019d059..b61a654 --- a/src/LoL-support/final_data.json +++ b/src/LoL-support/final_data.json @@ -1,1233 +1,934 @@ [ { - "champion_name": "Taric", - "pick_rate": "1.4%", - "counters": { - "Soraka": "55.26%", - "Rell": "52.91%", - "Vel'Koz": "52.75%", - "Janna": "50.88%", - "Zilean": "50.55%", - "Rakan": "49.62%", - "Maokai": "49.58%", - "Nami": "49.42%", - "Xerath": "49.28%", - "Senna": "49.24%" + "champion_name": "Maokai", + "pick_rate": "12.7%", + "counters": { + "Taric": "50.48%", + "Alistar": "49.83%", + "Braum": "49.49%", + "Leona": "49.02%", + "Janna": "48.98%", + "Renata Glasc": "48.48%", + "Rell": "47.97%", + "Shaco": "47.18%", + "Camille": "47.04%", + "Zyra": "47.02%" }, - "synergy": { - "Karthus": "70.83%", - "Yasuo": "68.91%", - "Lee Sin": "68.18%", - "Master Yi": "59.26%", - "Kog'Maw": "58.82%", - "Sivir": "58.62%", - "Varus": "56.63%", - "Swain": "56.45%", - "Seraphine": "56.41%", - "Kalista": "55.94%", - "Ziggs": "55.00%", - "Vayne": "54.01%", - "Jinx": "52.38%", - "Lucian": "51.91%", - "Kai'Sa": "51.81%", - "Ashe": "51.67%", - "Miss Fortune": "50.94%", - "Ezreal": "50.20%", - "Twitch": "50.00%" - } + "synergy": [ + "no data" + ] }, { "champion_name": "Janna", - "pick_rate": "9.6%", - "counters": { - "Senna": "50.79%", - "Sona": "50.1%", - "Soraka": "50.01%", - "Seraphine": "49.87%", - "Milio": "49.71%", - "Neeko": "49.67%", - "Rell": "49.5%", - "Bard": "49.44%", - "Rakan": "49.42%", - "Blitzcrank": "49.4%" - }, - "synergy": { - "Seraphine": "58.66%", - "Vayne": "55.43%", - "Yasuo": "54.91%", - "Ziggs": "54.85%", - "Jinx": "53.96%", - "Ashe": "53.85%", - "Tristana": "53.63%", - "Kalista": "53.47%", - "Kog'Maw": "52.89%", - "Karthus": "52.83%", - "Jhin": "52.65%", - "Ezreal": "52.58%", - "Lucian": "52.51%", - "Draven": "52.25%", - "Twitch": "52.00%", - "Samira": "51.88%", - "Varus": "51.73%", - "Zeri": "51.64%", - "Caitlyn": "51.53%", - "Kai'Sa": "50.95%" + "pick_rate": "10.7%", + "counters": { + "Camille": "53.25%", + "Sona": "51.89%", + "Blitzcrank": "51.2%", + "Maokai": "51.02%", + "Braum": "49.67%", + "Pyke": "49.38%", + "Renata Glasc": "49.3%", + "Senna": "49.25%", + "Ashe": "49.07%", + "Nami": "48.64%" + }, + "synergy": { + "Nilah": "57.38%", + "Twitch": "57.02%", + "Senna": "56.68%", + "Seraphine": "55.59%", + "Miss Fortune": "54.71%", + "Zeri": "54.30%", + "Tristana": "54.22%", + "Draven": "54.22%", + "Twisted Fate": "53.57%", + "Samira": "53.57%", + "Jhin": "53.03%", + "Lucian": "52.84%", + "Karthus": "52.73%", + "Ezreal": "52.60%", + "Yasuo": "52.58%", + "Kalista": "52.35%", + "Vayne": "51.97%", + "Ashe": "51.80%", + "Sivir": "51.10%", + "Caitlyn": "51.05%", + "Smolder": "50.23%", + "Aphelios": "50.23%" } }, { - "champion_name": "Maokai", - "pick_rate": "1.6%", + "champion_name": "Taric", + "pick_rate": "1.5%", "counters": { - "Shaco": "58.18%", - "Braum": "54.8%", - "Sona": "53.99%", - "Janna": "52.72%", - "Renata Glasc": "52.39%", - "Zyra": "51.46%", - "Alistar": "51.45%", - "Soraka": "50.8%", - "Taric": "50.42%", - "Neeko": "50.29%" - }, - "synergy": { - "Xayah": "60.00%", - "Ziggs": "58.33%", - "Karthus": "57.89%", - "Kai'Sa": "53.95%", - "Vayne": "53.51%", - "Sivir": "52.63%", - "Jhin": "52.46%", - "Aphelios": "52.46%", - "Draven": "52.38%", - "Ezreal": "52.29%", - "Samira": "51.81%", - "Varus": "51.53%", - "Zeri": "51.02%", - "Twitch": "50.00%", - "Ashe": "50.00%", - "Hwei": "50.00%" + "Swain": "57.63%", + "Sona": "57.32%", + "Janna": "54.88%", + "Nami": "54.27%", + "Lux": "53.54%", + "Seraphine": "53.45%", + "Bard": "52.97%", + "Soraka": "52.32%", + "Thresh": "52.21%", + "Braum": "51.67%" + }, + "synergy": { + "Vayne": "60.16%", + "Ashe": "58.46%", + "Yasuo": "58.33%", + "Twisted Fate": "57.89%", + "Pyke": "57.14%", + "Senna": "56.59%", + "Nilah": "56.32%", + "Kalista": "55.13%", + "Jinx": "54.39%", + "Aphelios": "53.57%", + "Seraphine": "53.13%", + "Zeri": "52.54%", + "Twitch": "52.05%", + "Smolder": "51.89%", + "Ezreal": "51.67%", + "Kai'Sa": "50.59%", + "Sivir": "50.00%", + "Lucian": "50.00%", + "Dr. Mundo": "50.00%", + "Xayah": "50.00%", + "Karthus": "50.00%" } }, { - "champion_name": "Bard", - "pick_rate": "6.1%", - "counters": { - "Taric": "52.2%", - "Janna": "50.56%", - "Maokai": "50.28%", - "Zilean": "49.95%", - "Senna": "49.94%", - "Blitzcrank": "49.89%", - "Renata Glasc": "49.52%", - "Soraka": "49.45%", - "Rell": "49.3%", - "Pyke": "49.06%" - }, - "synergy": { - "Nilah": "59.77%", - "Vayne": "55.15%", - "Caitlyn": "55.12%", - "Twitch": "55.06%", - "Ziggs": "54.37%", - "Tristana": "53.92%", - "Karthus": "53.62%", - "Seraphine": "53.05%", - "Lucian": "52.97%", - "Ezreal": "52.51%", - "Ashe": "52.44%", - "Jhin": "51.89%", - "Swain": "51.69%", - "Jinx": "51.34%", - "Zeri": "50.76%" - } + "champion_name": "Braum", + "pick_rate": "3.8%", + "counters": "no info", + "synergy": [ + "no data" + ] }, { - "champion_name": "Rakan", - "pick_rate": "12.8%", - "counters": { - "Senna": "52.31%", - "Bard": "51.42%", - "Soraka": "51.41%", - "Neeko": "51.37%", - "Renata Glasc": "51.09%", - "Sona": "50.93%", - "Maokai": "50.87%", - "Rell": "50.69%", - "Zilean": "50.64%", - "Janna": "50.58%" - }, - "synergy": { - "Lucian": "56.02%", - "Jinx": "54.48%", - "Twitch": "54.35%", - "Seraphine": "54.17%", - "Ziggs": "54.08%", - "Vayne": "54.03%", - "Sivir": "53.73%", - "Jhin": "52.40%", - "Yasuo": "51.97%", - "Karthus": "51.90%", - "Ezreal": "51.77%", - "Ashe": "51.55%", - "Miss Fortune": "50.65%", - "Nilah": "50.61%", - "Varus": "50.35%", - "Draven": "50.12%" + "champion_name": "Blitzcrank", + "pick_rate": "8.1%", + "counters": { + "Pantheon": "54.85%", + "Leona": "54.08%", + "Maokai": "53.66%", + "Braum": "53.61%", + "Rakan": "53.26%", + "Taric": "53.13%", + "Alistar": "52.33%", + "Shaco": "51.5%", + "Rell": "50.9%", + "Zilean": "50.57%" + }, + "synergy": { + "Seraphine": "65.22%", + "Karthus": "57.78%", + "Draven": "56.57%", + "Vayne": "55.39%", + "Varus": "54.74%", + "Jinx": "54.30%", + "Twisted Fate": "54.17%", + "Tristana": "53.18%", + "Sivir": "53.06%", + "Senna": "52.85%", + "Aphelios": "52.14%", + "Kai'Sa": "51.65%", + "Lucian": "50.78%", + "Nilah": "50.67%", + "Twitch": "50.00%", + "Ashe": "50.00%", + "Zeri": "50.00%" } }, { - "champion_name": "Sona", - "pick_rate": "2.4%", + "champion_name": "Camille", + "pick_rate": "2.3%", "counters": { - "Taric": "56.83%", - "Leona": "55.04%", - "Seraphine": "53.49%", - "Blitzcrank": "52.56%", - "Senna": "51.71%", - "Bard": "51.4%", - "Nami": "50.59%", - "Milio": "50.43%", - "Zilean": "50.26%", - "Pyke": "50.19%" - }, - "synergy": { - "Yasuo": "64.52%", - "Tristana": "60.29%", - "Ziggs": "58.51%", - "Samira": "57.45%", - "Sivir": "56.60%", - "Vayne": "56.55%", - "Nilah": "55.88%", - "Kog'Maw": "53.49%", - "Lucian": "53.04%", - "Xayah": "52.90%", - "Seraphine": "52.76%", - "Ezreal": "52.71%", - "Miss Fortune": "52.23%", - "Varus": "51.05%", - "Ashe": "50.93%", - "Caitlyn": "50.81%", - "Twitch": "50.21%" + "Taric": "58.76%", + "Renata Glasc": "58.12%", + "Braum": "57.32%", + "Leona": "54.48%", + "Brand": "54.2%", + "Bard": "53.76%", + "Zac": "53.03%", + "Maokai": "52.96%", + "Tahm Kench": "52.94%", + "Zilean": "52.78%" + }, + "synergy": { + "Twitch": "62.67%", + "Sivir": "58.33%", + "Zeri": "56.47%", + "Tristana": "56.20%", + "Senna": "55.81%", + "Xayah": "55.26%", + "Aphelios": "53.62%", + "Nilah": "53.13%", + "Seraphine": "52.94%", + "Varus": "52.89%", + "Karthus": "52.63%", + "Ashe": "52.50%", + "Smolder": "52.34%", + "Jhin": "52.09%", + "Draven": "52.06%", + "Ziggs": "52.00%", + "Twisted Fate": "50.94%", + "Vayne": "50.92%", + "Lucian": "50.35%", + "Swain": "50.00%" } }, { - "champion_name": "Senna", - "pick_rate": "11.0%", - "counters": { - "Pyke": "52.31%", - "Blitzcrank": "52.25%", - "Xerath": "51.66%", - "Maokai": "51.62%", - "Zyra": "51.1%", - "Taric": "50.76%", - "Leona": "50.22%", - "Bard": "50.06%", - "Lux": "49.9%", - "Rell": "49.8%" - }, + "champion_name": "Leona", + "pick_rate": "4.4%", + "counters": "no info", "synergy": { - "Kalista": "57.48%", - "Nilah": "56.48%", - "Karthus": "56.18%", - "Zeri": "55.11%", - "Seraphine": "55.06%", - "Miss Fortune": "54.98%", - "Cho'Gath": "54.69%", - "Vayne": "54.60%", - "Ashe": "54.54%", - "Tahm Kench": "54.51%", - "Yasuo": "54.49%", - "Brand": "54.26%", - "Twitch": "54.15%", - "Swain": "53.87%", - "Jhin": "52.87%", - "Ziggs": "52.59%", - "Sivir": "52.21%", - "Ezreal": "51.36%", - "Tristana": "51.36%", - "Varus": "50.75%" + "Swain": "68.09%", + "Senna": "60.41%", + "Jinx": "59.02%", + "Seraphine": "58.90%", + "Ashe": "57.67%", + "Aphelios": "56.67%", + "Nilah": "56.52%", + "Vayne": "55.20%", + "Lucian": "54.63%", + "Smolder": "54.42%", + "Miss Fortune": "53.39%", + "Karthus": "53.06%", + "Xayah": "51.61%", + "Ezreal": "51.04%", + "Kalista": "50.59%", + "Twisted Fate": "50.31%" } }, { "champion_name": "Rell", - "pick_rate": "4.9%", - "counters": { - "Poppy": "53.79%", - "Seraphine": "53.57%", - "Soraka": "52.57%", - "Zilean": "52.54%", - "Alistar": "52.45%", - "Shaco": "51.5%", - "Maokai": "50.89%", - "Sona": "50.75%", - "Bard": "50.7%", - "Janna": "50.5%" - }, + "pick_rate": "4.1%", + "counters": "no info", "synergy": { - "Ziggs": "61.31%", - "Karthus": "60.18%", - "Seraphine": "59.02%", - "Tristana": "58.16%", - "Lucian": "57.30%", - "Twitch": "56.30%", - "Miss Fortune": "54.44%", - "Ashe": "53.88%", - "Sivir": "53.49%", - "Jinx": "52.83%", - "Draven": "52.25%", - "Xayah": "52.10%", - "Ezreal": "52.08%", - "Kai'Sa": "51.33%", - "Samira": "51.15%", - "Zeri": "50.35%", - "Yasuo": "50.25%", - "Aphelios": "50.21%" + "Xayah": "58.33%", + "Twisted Fate": "56.72%", + "Twitch": "56.48%", + "Nilah": "56.25%", + "Miss Fortune": "55.88%", + "Zeri": "55.45%", + "Senna": "55.08%", + "Vayne": "53.65%", + "Jinx": "53.40%", + "Kalista": "52.46%", + "Samira": "52.46%", + "Aphelios": "52.17%", + "Draven": "52.15%", + "Tristana": "51.90%", + "Karthus": "51.43%", + "Ezreal": "51.32%", + "Jhin": "50.43%", + "Yasuo": "50.00%" } }, { - "champion_name": "Neeko", - "pick_rate": "1.9%", - "counters": { - "Vel'Koz": "60.45%", - "Sona": "53.26%", - "Bard": "52.61%", - "Brand": "52.38%", - "Taric": "52.17%", - "Alistar": "51%", - "Shaco": "50.68%", - "Senna": "50.42%", - "Renata Glasc": "50.38%", - "Morgana": "50.35%" - }, - "synergy": { - "Nilah": "77.78%", - "Yasuo": "59.46%", - "Xayah": "59.26%", - "Hwei": "58.82%", - "Lucian": "56.79%", - "Vayne": "53.85%", - "Twitch": "53.17%", - "Kalista": "50.75%", - "Ezreal": "50.72%", - "Ashe": "50.29%", - "Quinn": "50.00%", - "Samira": "50.00%", - "Varus": "50.00%" - } + "champion_name": "Rakan", + "pick_rate": "7.7%", + "counters": "no info", + "synergy": [ + "no data" + ] }, { - "champion_name": "Soraka", - "pick_rate": "5.5%", - "counters": { - "Blitzcrank": "54.17%", - "Neeko": "51.31%", - "Sona": "51.25%", - "Zyra": "51.13%", - "Vel'Koz": "51.05%", - "Bard": "50.55%", - "Senna": "50.38%", - "Yuumi": "50.06%", - "Janna": "49.99%", - "Pyke": "49.98%" - }, + "champion_name": "Zilean", + "pick_rate": "2.8%", + "counters": "no info", "synergy": { - "Yasuo": "63.54%", - "Swain": "59.70%", - "Samira": "58.41%", - "Vayne": "57.03%", - "Ashe": "53.73%", - "Jinx": "53.47%", - "Ziggs": "53.38%", - "Jhin": "53.17%", - "Tristana": "52.80%", - "Lucian": "52.34%", - "Miss Fortune": "51.96%", - "Twitch": "51.63%", - "Draven": "50.30%" + "Miss Fortune": "62.03%", + "Twisted Fate": "61.05%", + "Draven": "54.63%", + "Swain": "54.55%", + "Vayne": "54.47%", + "Ashe": "53.97%", + "Caitlyn": "53.13%", + "Seraphine": "52.44%", + "Smolder": "51.37%", + "Xayah": "51.22%", + "Sivir": "50.00%" } }, { - "champion_name": "Blitzcrank", - "pick_rate": "8.8%", - "counters": { - "Taric": "56.85%", - "Rakan": "52.88%", - "Leona": "52.47%", - "Braum": "52.28%", - "Renata Glasc": "51.05%", - "Maokai": "51.04%", - "Zyra": "50.98%", - "Rell": "50.78%", - "Alistar": "50.73%", - "Zilean": "50.7%" - }, + "champion_name": "Pyke", + "pick_rate": "8.5%", + "counters": "no info", "synergy": { - "Nilah": "58.59%", - "Xayah": "55.50%", - "Draven": "54.17%", - "Lucian": "53.85%", - "Ashe": "53.42%", - "Vayne": "52.91%", - "Twitch": "52.80%", - "Seraphine": "52.34%", - "Jinx": "51.90%", - "Karthus": "51.89%", - "Varus": "51.87%", - "Tristana": "51.72%", - "Miss Fortune": "51.53%", - "Zeri": "51.27%", - "Aphelios": "50.95%", - "Kalista": "50.84%", - "Jhin": "50.74%", - "Ezreal": "50.08%", - "Caitlyn": "50.00%" + "Karthus": "60.47%", + "Swain": "58.06%", + "Senna": "57.89%", + "Ashe": "55.02%", + "Twitch": "54.55%", + "Vayne": "53.90%", + "Jinx": "53.67%", + "Yasuo": "53.66%", + "Seraphine": "53.19%", + "Twisted Fate": "53.07%", + "Nilah": "53.00%", + "Smolder": "51.63%", + "Zeri": "50.56%", + "Ezreal": "50.22%", + "Draven": "50.00%" } }, { - "champion_name": "Zyra", - "pick_rate": "3.8%", + "champion_name": "Zac", + "pick_rate": "1.0%", "counters": { - "Sona": "53.56%", - "Pyke": "53.25%", - "Heimerdinger": "53.11%", - "Bard": "53.07%", - "Leona": "51.91%", - "Janna": "51.88%", - "Shaco": "51.56%", - "Taric": "51.43%", - "Rakan": "50.98%", - "Nautilus": "50.75%" - }, - "synergy": { - "Seraphine": "55.06%", - "Vayne": "54.84%", - "Tristana": "54.05%", - "Twitch": "52.96%", - "Samira": "51.98%", - "Jhin": "51.51%", - "Karthus": "51.16%", - "Draven": "50.65%", + "Pantheon": "61.36%", + "Taric": "58.82%", + "Zilean": "58.49%", + "Rell": "56.82%", + "Bard": "56.55%", + "Morgana": "55.93%", + "Janna": "54.44%", + "Maokai": "53.44%", + "Milio": "53.25%", + "Thresh": "53.18%" + }, + "synergy": { + "Aphelios": "83.33%", + "Caitlyn": "66.67%", + "Draven": "65.52%", + "Senna": "64.71%", + "Tristana": "61.54%", + "Twisted Fate": "61.29%", + "Zeri": "58.82%", + "Seraphine": "56.25%", + "Jinx": "55.00%", + "Ezreal": "52.38%", + "Smolder": "51.58%", + "Kalista": "51.52%", "Xayah": "50.00%" } }, { - "champion_name": "Pyke", - "pick_rate": "7.6%", - "counters": { - "Rakan": "53.38%", - "Maokai": "52.78%", - "Neeko": "52.1%", - "Rell": "52.08%", - "Taric": "51.82%", - "Janna": "51.79%", - "Blitzcrank": "51.44%", - "Alistar": "50.94%", - "Bard": "50.94%", - "Thresh": "50.49%" - }, - "synergy": { - "Karthus": "61.98%", - "Nilah": "60.43%", - "Yasuo": "57.30%", - "Tristana": "55.96%", - "Twitch": "55.08%", - "Ashe": "54.27%", - "Swain": "53.91%", - "Ziggs": "53.28%", - "Seraphine": "52.82%", - "Vayne": "52.64%", - "Varus": "51.58%", - "Jhin": "51.51%", - "Ezreal": "51.49%", - "Hwei": "51.43%", - "Draven": "50.95%" - } - }, - { - "champion_name": "Zilean", - "pick_rate": "2.5%", - "counters": { - "Janna": "53.27%", - "Shaco": "53.02%", - "Senna": "51.99%", - "Neeko": "51.67%", - "Maokai": "51.41%", - "Xerath": "51.07%", - "Milio": "50.67%", - "Sylas": "50.34%", - "Soraka": "50.28%", - "Vel'Koz": "50.23%" - }, + "champion_name": "Alistar", + "pick_rate": "4.7%", + "counters": "no info", "synergy": { - "Swain": "58.97%", - "Hwei": "55.56%", - "Samira": "54.12%", - "Kog'Maw": "54.05%", - "Twitch": "53.80%", - "Aphelios": "53.47%", - "Vayne": "51.88%", - "Caitlyn": "51.79%", - "Seraphine": "51.67%", - "Ezreal": "51.07%", - "Ashe": "50.12%" + "Karthus": "64.86%", + "Seraphine": "62.86%", + "Zeri": "60.83%", + "Sivir": "59.26%", + "Jinx": "58.42%", + "Yasuo": "58.09%", + "Tristana": "55.84%", + "Senna": "53.27%", + "Twitch": "52.99%", + "Draven": "51.74%", + "Xayah": "51.25%", + "Ezreal": "50.91%", + "Twisted Fate": "50.75%", + "Samira": "50.00%" } }, { - "champion_name": "Braum", - "pick_rate": "3.6%", + "champion_name": "Bard", + "pick_rate": "5.7%", "counters": { - "Zilean": "55.95%", - "Soraka": "54.2%", - "Bard": "53.76%", - "Rell": "53.74%", - "Taric": "53.44%", - "Sona": "53.2%", - "Senna": "53.06%", - "Rakan": "52.88%", - "Janna": "52.49%", - "Renata Glasc": "52.31%" - }, - "synergy": { - "Sivir": "60.82%", - "Twitch": "59.55%", - "Xayah": "54.40%", - "Varus": "52.69%", - "Kog'Maw": "52.03%", - "Ashe": "51.92%", - "Jinx": "51.32%", - "Tristana": "50.94%", - "Vayne": "50.83%", - "Draven": "50.75%", - "Ezreal": "50.35%" + "Maokai": "56.1%", + "Renata Glasc": "53.77%", + "Janna": "53.45%", + "Pyke": "51.79%", + "Morgana": "51.04%", + "Nami": "50.92%", + "Pantheon": "50.72%", + "Blitzcrank": "50.43%", + "Brand": "50.34%", + "Senna": "50.21%" + }, + "synergy": { + "Yasuo": "60.34%", + "Seraphine": "59.53%", + "Senna": "59.42%", + "Ziggs": "57.58%", + "Caitlyn": "55.43%", + "Xayah": "54.26%", + "Hwei": "54.17%", + "Twisted Fate": "53.66%", + "Jinx": "52.43%", + "Twitch": "51.68%", + "Aphelios": "51.54%", + "Karthus": "51.47%", + "Samira": "50.60%", + "Vayne": "50.34%" } }, { - "champion_name": "Renata Glasc", - "pick_rate": "5.0%", + "champion_name": "Soraka", + "pick_rate": "5.4%", "counters": "no info", "synergy": [ "no data" ] }, { - "champion_name": "Vel'Koz", - "pick_rate": "1.6%", - "counters": { - "Pyke": "55.14%", - "Zyra": "54.76%", - "Seraphine": "54.15%", - "Blitzcrank": "53.89%", - "Janna": "53.62%", - "Bard": "53.22%", - "Sona": "52.97%", - "Nami": "52.1%", - "Maokai": "51.85%", - "Renata Glasc": "51.64%" - }, - "synergy": { - "Karthus": "61.90%", - "Vayne": "55.34%", - "Sivir": "54.72%", - "Ashe": "54.44%", - "Aphelios": "53.25%", - "Seraphine": "52.38%", - "Jhin": "51.92%", - "Caitlyn": "51.43%", - "Ezreal": "51.22%", - "Twitch": "50.39%", - "Varus": "50.37%", - "Tristana": "50.00%", - "Miss Fortune": "50.00%" + "champion_name": "Sona", + "pick_rate": "2.3%", + "counters": { + "Zilean": "61.68%", + "Rell": "59.62%", + "Leona": "57.21%", + "Neeko": "55.71%", + "Thresh": "55.03%", + "Maokai": "54.7%", + "Bard": "53.59%", + "Braum": "51.81%", + "Blitzcrank": "51.79%", + "Seraphine": "51.2%" + }, + "synergy": { + "Nilah": "71.43%", + "Tristana": "63.16%", + "Twisted Fate": "56.19%", + "Jhin": "55.34%", + "Kai'Sa": "55.11%", + "Varus": "52.63%", + "Aphelios": "52.00%", + "Twitch": "51.46%", + "Vayne": "51.22%", + "Smolder": "50.72%", + "Lucian": "50.00%", + "Samira": "50.00%" } }, { "champion_name": "Thresh", - "pick_rate": "13.3%", - "counters": { - "Taric": "53.39%", - "Janna": "52.43%", - "Brand": "52.12%", - "Rakan": "52.02%", - "Neeko": "51.75%", - "Sona": "51.47%", - "Maokai": "51.34%", - "Vel'Koz": "51.27%", - "Bard": "51.24%", - "Renata Glasc": "51.24%" - }, + "pick_rate": "9.4%", + "counters": "no info", "synergy": { - "Tristana": "55.29%", - "Hwei": "54.01%", - "Draven": "53.28%", - "Jinx": "52.84%", - "Aphelios": "51.39%", - "Twitch": "51.28%", - "Kalista": "51.17%", - "Samira": "51.08%", - "Vayne": "51.01%", - "Karthus": "50.91%", - "Lucian": "50.61%", - "Ashe": "50.26%", - "Yasuo": "50.24%" + "Yasuo": "62.82%", + "Senna": "58.33%", + "Xayah": "56.52%", + "Sivir": "56.25%", + "Seraphine": "55.74%", + "Kalista": "54.41%", + "Twitch": "52.68%", + "Miss Fortune": "52.44%", + "Jinx": "52.43%", + "Draven": "52.37%", + "Aphelios": "52.12%", + "Smolder": "51.38%", + "Tristana": "51.11%", + "Samira": "50.59%", + "Varus": "50.32%", + "Vayne": "50.15%", + "Kai'Sa": "50.05%" } }, { - "champion_name": "Milio", - "pick_rate": "6.7%", - "counters": { - "Taric": "54.79%", - "Blitzcrank": "53.66%", - "Braum": "53.22%", - "Vel'Koz": "53.08%", - "Senna": "52.78%", - "Bard": "52.75%", - "Neeko": "52.17%", - "Rakan": "51.91%", - "Soraka": "51.52%", - "Thresh": "51.33%" - }, - "synergy": { - "Kog'Maw": "53.82%", - "Xayah": "52.76%", - "Lucian": "51.94%", - "Zeri": "51.56%", - "Vayne": "51.56%", - "Kalista": "51.56%", - "Ashe": "51.48%", - "Draven": "50.69%", - "Aphelios": "50.42%", - "Twitch": "50.00%" - } + "champion_name": "Renata Glasc", + "pick_rate": "2.4%", + "counters": "no info", + "synergy": [ + "no data" + ] }, { - "champion_name": "Alistar", - "pick_rate": "5.8%", - "counters": { - "Sona": "54.51%", - "Janna": "54.04%", - "Swain": "53.24%", - "Vel'Koz": "52.78%", - "Senna": "52.64%", - "Zilean": "52.39%", - "Soraka": "52.22%", - "Seraphine": "51.97%", - "Xerath": "51.86%", - "Milio": "51.85%" - }, - "synergy": { - "Seraphine": "59.43%", - "Ziggs": "57.86%", - "Sivir": "55.20%", - "Hwei": "54.65%", - "Zeri": "53.37%", - "Ashe": "52.55%", - "Yasuo": "52.42%", - "Vayne": "52.01%", - "Twitch": "51.83%", - "Xayah": "51.49%", - "Jinx": "50.82%", - "Jhin": "50.68%", - "Ezreal": "50.58%", - "Draven": "50.28%", - "Miss Fortune": "50.20%" + "champion_name": "Zyra", + "pick_rate": "4.3%", + "counters": { + "Taric": "54.73%", + "Leona": "54.05%", + "Sona": "53.31%", + "Maokai": "52.98%", + "Renata Glasc": "52.84%", + "Pyke": "52.6%", + "Zac": "52.59%", + "Janna": "52.42%", + "Zilean": "51.88%", + "Yuumi": "51.63%" + }, + "synergy": { + "Xayah": "66.07%", + "Sivir": "61.54%", + "Twisted Fate": "53.68%", + "Miss Fortune": "53.25%", + "Ezreal": "52.52%", + "Senna": "52.28%", + "Tristana": "50.94%", + "Vayne": "50.18%", + "Jinx": "50.00%" } }, { - "champion_name": "Zac", - "pick_rate": "0.7%", - "counters": { - "Poppy": "67.39%", - "Maokai": "57.14%", - "Neeko": "56.72%", - "Swain": "56%", - "Vel'Koz": "54.1%", - "Taric": "53.85%", - "Alistar": "53.74%", - "Ashe": "53.49%", - "Rakan": "53.42%", - "Zilean": "53.26%" - }, + "champion_name": "Vel'Koz", + "pick_rate": "1.4%", + "counters": "no info", "synergy": { - "Ziggs": "72.73%", - "Sivir": "71.43%", - "Zeri": "70.00%", - "Varus": "63.64%", - "Yasuo": "62.07%", - "Jhin": "59.32%", - "Kalista": "57.14%", - "Hwei": "57.14%", - "Twitch": "55.32%", - "Ezreal": "52.63%", - "Miss Fortune": "52.38%", - "Vayne": "50.75%", - "Swain": "50.00%", - "Tristana": "50.00%", - "Caitlyn": "50.00%" + "Samira": "65.00%", + "Nilah": "60.00%", + "Hwei": "58.82%", + "Seraphine": "54.55%", + "Draven": "53.66%", + "Ashe": "53.52%", + "Tristana": "53.33%", + "Zeri": "53.13%", + "Senna": "51.90%", + "Kai'Sa": "51.02%", + "Jhin": "50.71%", + "Smolder": "50.22%", + "Sivir": "50.00%", + "Karthus": "50.00%" } }, { - "champion_name": "Leona", - "pick_rate": "4.8%", - "counters": { - "Taric": "56.7%", - "Janna": "54.1%", - "Bard": "54.03%", - "Shaco": "53.67%", - "Maokai": "53.55%", - "Rell": "53.22%", - "Soraka": "53.11%", - "Lux": "53.09%", - "Morgana": "53.02%", - "Braum": "51.72%" - }, - "synergy": { - "Seraphine": "56.47%", - "Xayah": "55.00%", - "Zeri": "54.63%", - "Ezreal": "54.35%", - "Twitch": "54.27%", - "Caitlyn": "53.68%", - "Jhin": "53.21%", - "Yasuo": "53.19%", - "Lucian": "52.72%", - "Miss Fortune": "51.92%", - "Aphelios": "51.70%", - "Tristana": "51.48%", - "Kalista": "51.16%" - } + "champion_name": "Senna", + "pick_rate": "11.0%", + "counters": "no info", + "synergy": [ + "no data" + ] }, { - "champion_name": "Xerath", - "pick_rate": "4.8%", - "counters": { - "Sylas": "55%", - "Maokai": "53.98%", - "Sona": "53.77%", - "Janna": "53.15%", - "Pyke": "52.84%", - "Soraka": "52.33%", - "Rakan": "52.02%", - "Leona": "51.76%", - "Zyra": "51.72%", - "Nami": "51.6%" - }, + "champion_name": "Shaco", + "pick_rate": "2.6%", + "counters": "no info", "synergy": { - "Ziggs": "55.24%", - "Jinx": "53.95%", - "Vayne": "53.32%", - "Samira": "52.63%", - "Xayah": "52.57%", - "Ashe": "52.33%", - "Jhin": "51.81%", - "Caitlyn": "51.12%", - "Hwei": "50.88%", - "Tristana": "50.70%", - "Ezreal": "50.00%" + "Karthus": "76.92%", + "Twisted Fate": "62.64%", + "Tristana": "58.62%", + "Senna": "56.19%", + "Miss Fortune": "56.10%", + "Aphelios": "52.78%", + "Hwei": "52.63%", + "Ashe": "52.48%", + "Draven": "52.24%", + "Nilah": "50.00%", + "Caitlyn": "50.00%", + "Ziggs": "50.00%" } }, { - "champion_name": "Nami", - "pick_rate": "6.5%", - "counters": { - "Maokai": "56.37%", - "Neeko": "53.14%", - "Blitzcrank": "53.03%", - "Rakan": "52.58%", - "Zyra": "52.29%", - "Soraka": "51.92%", - "Milio": "51.67%", - "Pyke": "51.56%", - "Janna": "51.44%", - "Bard": "51.33%" - }, + "champion_name": "Pantheon", + "pick_rate": "1.8%", + "counters": "no info", "synergy": { - "Swain": "59.52%", - "Brand": "55.69%", - "Karthus": "55.65%", - "Twitch": "52.16%", - "Ashe": "51.88%", - "Seraphine": "51.43%", - "Jhin": "51.32%", - "Vayne": "51.29%", - "Ezreal": "51.27%" + "Karthus": "66.67%", + "Twisted Fate": "58.82%", + "Miss Fortune": "56.60%", + "Senna": "56.59%", + "Jhin": "55.67%", + "Nilah": "54.55%", + "Ezreal": "54.37%", + "Swain": "53.85%", + "Lucian": "53.06%", + "Ashe": "52.38%", + "Smolder": "52.07%", + "Samira": "51.97%", + "Jinx": "51.72%" } }, { - "champion_name": "Shaco", + "champion_name": "Tahm Kench", "pick_rate": "1.1%", - "counters": { - "Sona": "60.95%", - "Swain": "60.22%", - "Taric": "59.68%", - "Heimerdinger": "59.02%", - "Zac": "56.14%", - "Janna": "55.96%", - "Milio": "54.49%", - "Pyke": "54.23%", - "Rakan": "53.2%", - "Bard": "52.59%" - }, + "counters": "no info", "synergy": { - "Seraphine": "85.71%", - "Miss Fortune": "58.33%", - "Samira": "54.84%", - "Caitlyn": "54.43%", - "Jhin": "54.01%", - "Varus": "52.43%", - "Karthus": "50.00%" + "Kalista": "68.75%", + "Jinx": "65.00%", + "Nilah": "62.50%", + "Xayah": "62.50%", + "Twitch": "57.14%", + "Varus": "55.10%", + "Twisted Fate": "54.55%", + "Zeri": "53.85%", + "Senna": "52.51%", + "Seraphine": "50.00%" } }, { - "champion_name": "Heimerdinger", - "pick_rate": "1.0%", + "champion_name": "Nautilus", + "pick_rate": "11.0%", "counters": { - "Maokai": "61.64%", - "Neeko": "56.82%", - "Seraphine": "55.95%", - "Xerath": "54.86%", - "Bard": "54.55%", - "Leona": "54.4%", - "Rell": "53.15%", - "Rakan": "52.67%", - "Lulu": "52.56%", - "Janna": "52.56%" - }, - "synergy": { - "Zeri": "68.00%", - "Sivir": "57.89%", - "Draven": "54.29%", - "Jhin": "54.03%", - "Vayne": "53.49%", - "Miss Fortune": "52.78%", - "Ashe": "50.34%", - "Kalista": "50.00%", - "Twitch": "50.00%" + "Taric": "60.99%", + "Leona": "57.54%", + "Braum": "56.73%", + "Renata Glasc": "55.96%", + "Alistar": "55.65%", + "Rell": "55.38%", + "Camille": "54.74%", + "Pantheon": "54.53%", + "Swain": "54.35%", + "Maokai": "53.72%" + }, + "synergy": { + "Seraphine": "55.21%", + "Senna": "54.59%", + "Kalista": "52.55%", + "Xayah": "52.25%", + "Caitlyn": "51.40%", + "Karthus": "51.11%", + "Twitch": "50.96%", + "Lucian": "50.86%", + "Jhin": "50.64%", + "Draven": "50.62%", + "Vayne": "50.27%", + "Zeri": "50.21%", + "Samira": "50.09%", + "Nilah": "50.00%" } }, { - "champion_name": "Sylas", - "pick_rate": "1.0%", - "counters": { - "Sona": "60%", - "Janna": "57.91%", - "Vel'Koz": "57.75%", - "Braum": "57.52%", - "Morgana": "57.5%", - "Taric": "55.56%", - "Rell": "54.55%", - "Rakan": "53.89%", - "Blitzcrank": "53.74%", - "Thresh": "53.64%" + "champion_name": "Milio", + "pick_rate": "8.1%", + "counters": { + "Blitzcrank": "56.32%", + "Braum": "55.45%", + "Maokai": "54.37%", + "Camille": "53.86%", + "Janna": "53.43%", + "Thresh": "53.37%", + "Zilean": "53.04%", + "Tahm Kench": "53.04%", + "Vel'Koz": "52.98%", + "Leona": "52.96%" }, - "synergy": { - "Karthus": "60.00%", - "Aphelios": "58.06%", - "Nilah": "57.89%", - "Miss Fortune": "57.14%", - "Kalista": "55.36%", - "Lucian": "54.65%", - "Seraphine": "54.55%", - "Sivir": "52.94%", - "Zeri": "52.63%", - "Vayne": "52.53%", - "Jhin": "52.38%", - "Ezreal": "51.14%", - "Twitch": "50.77%", - "Yasuo": "50.00%" - } + "synergy": [ + "no data" + ] }, { - "champion_name": "Tahm Kench", - "pick_rate": "0.6%", + "champion_name": "Nami", + "pick_rate": "5.5%", "counters": { - "Taric": "62.22%", - "Braum": "61.11%", - "Bard": "57.4%", - "Zyra": "57.27%", - "Pyke": "55.38%", - "Maokai": "54.9%", - "Brand": "54.67%", - "Lulu": "54.55%", - "Renata Glasc": "54.17%", - "Rakan": "54.12%" + "Morgana": "56.08%", + "Blitzcrank": "55.61%", + "Maokai": "55.57%", + "Vel'Koz": "54.47%", + "Rakan": "54.44%", + "Neeko": "54.21%", + "Rell": "53.9%", + "Nautilus": "53.32%", + "Thresh": "53.28%", + "Senna": "53.17%" }, - "synergy": { - "Veigar": "80.00%", - "Ziggs": "75.00%", - "Karthus": "66.67%", - "Seraphine": "58.33%", - "Xayah": "58.33%", - "Jhin": "55.56%", - "Miss Fortune": "51.72%", - "Twitch": "51.28%", - "Kog'Maw": "50.00%", - "Kalista": "50.00%", - "Ashe": "50.00%", - "Ezreal": "50.00%" + "synergy": [ + "no data" + ] + }, + { + "champion_name": "Xerath", + "pick_rate": "2.7%", + "counters": { + "Blitzcrank": "58.63%", + "Neeko": "56.43%", + "Sona": "56.36%", + "Maokai": "56.26%", + "Pyke": "56.15%", + "Leona": "56.02%", + "Braum": "55.06%", + "Rakan": "54.83%", + "Nautilus": "54.65%", + "Janna": "53.86%" + }, + "synergy": { + "Samira": "70.00%", + "Twisted Fate": "61.36%", + "Caitlyn": "57.97%", + "Karthus": "55.56%", + "Ashe": "52.46%", + "Vayne": "50.55%", + "Zeri": "50.00%" } }, { - "champion_name": "Amumu", - "pick_rate": "0.6%", - "counters": { - "Maokai": "66.07%", - "Zyra": "61.68%", - "Rell": "58.9%", - "Rakan": "58.55%", - "Janna": "58.5%", - "Neeko": "57.14%", - "Taric": "56.1%", - "Braum": "55.41%", - "Bard": "54%", - "Leona": "53.91%" - }, + "champion_name": "Brand", + "pick_rate": "1.9%", + "counters": "no info", "synergy": { - "Yasuo": "71.43%", - "Ziggs": "66.67%", - "Kalista": "64.15%", - "Lucian": "60.00%", - "Karthus": "60.00%", - "Hwei": "57.14%", - "Ezreal": "55.88%", - "Nilah": "55.56%", - "Draven": "55.41%", - "Miss Fortune": "54.37%", - "Ashe": "54.17%", - "Jinx": "53.85%", - "Kai'Sa": "53.60%", - "Tristana": "52.63%", - "Zeri": "50.00%", - "Veigar": "50.00%" + "Samira": "59.09%", + "Yasuo": "57.89%", + "Twisted Fate": "57.14%", + "Xayah": "56.52%", + "Jinx": "55.88%", + "Jhin": "53.98%", + "Draven": "53.70%", + "Seraphine": "52.50%", + "Aphelios": "50.00%" } }, { - "champion_name": "Twitch", - "pick_rate": "0.8%", + "champion_name": "Swain", + "pick_rate": "1.4%", "counters": { - "Zilean": "61.97%", - "Taric": "60.47%", - "Leona": "57.92%", - "Soraka": "57.87%", - "Shaco": "57.38%", + "Zac": "58.97%", + "Sona": "58.67%", + "Neeko": "57.41%", "Nami": "57.14%", - "Rakan": "56.34%", - "Sylas": "55.56%", - "Brand": "55.26%", - "Pyke": "54.65%" + "Milio": "56.44%", + "Pantheon": "55.95%", + "Zyra": "55.49%", + "Morgana": "55.24%", + "Leona": "55%", + "Camille": "54.79%" }, - "synergy": { - "Sivir": "70.97%", - "Swain": "70.00%", - "Seraphine": "63.64%", - "Draven": "57.89%", - "Cassiopeia": "53.85%", - "Vayne": "53.54%", - "Jhin": "53.40%", - "Jinx": "52.27%", - "Ziggs": "51.72%", - "Zeri": "51.61%", - "Nilah": "50.00%", - "Kalista": "50.00%", - "Hwei": "50.00%", - "Tristana": "50.00%" - } + "synergy": [ + "no data" + ] }, { - "champion_name": "Nautilus", - "pick_rate": "10.9%", - "counters": { - "Taric": "55.98%", - "Rell": "55.66%", - "Renata Glasc": "55.13%", - "Sylas": "55.09%", - "Braum": "54.08%", - "Rakan": "54.06%", - "Vel'Koz": "53.45%", - "Swain": "53.09%", - "Neeko": "52.97%", - "Alistar": "52.87%" + "champion_name": "Lulu", + "pick_rate": "7.9%", + "counters": { + "Vel'Koz": "57.35%", + "Blitzcrank": "56.23%", + "Zilean": "55.78%", + "Tahm Kench": "55.43%", + "Renata Glasc": "54.86%", + "Rell": "54.7%", + "Janna": "54.6%", + "Maokai": "54.51%", + "Braum": "53.79%", + "Ashe": "53.65%" }, - "synergy": { - "Seraphine": "58.68%", - "Ziggs": "55.43%", - "Karthus": "55.21%", - "Lucian": "51.71%", - "Tristana": "51.50%", - "Samira": "51.14%", - "Kalista": "50.71%", - "Jinx": "50.13%" - } + "synergy": [ + "no data" + ] }, { - "champion_name": "Ashe", - "pick_rate": "5.7%", - "counters": { - "Pyke": "56.98%", - "Sylas": "54.95%", - "Blitzcrank": "54.83%", - "Heimerdinger": "54.8%", - "Rell": "54.67%", - "Nautilus": "54.35%", - "Bard": "53.44%", - "Milio": "53.1%", - "Rakan": "52.69%", - "Brand": "52.5%" - }, + "champion_name": "Morgana", + "pick_rate": "2.8%", + "counters": "no info", "synergy": { - "Karthus": "56.83%", - "Seraphine": "54.60%", - "Jinx": "54.17%", - "Nilah": "53.85%", - "Twitch": "53.55%", - "Ezreal": "51.69%", - "Swain": "51.32%", - "Ziggs": "50.79%", - "Lucian": "50.00%", - "Zeri": "50.00%" + "Xayah": "60.00%", + "Draven": "57.81%", + "Nilah": "54.55%", + "Samira": "51.61%", + "Sivir": "50.00%", + "Seraphine": "50.00%" } }, { - "champion_name": "Lux", - "pick_rate": "5.1%", - "counters": { - "Blitzcrank": "56.57%", - "Maokai": "54.94%", - "Zyra": "54.66%", - "Shaco": "54.45%", - "Vel'Koz": "54.1%", - "Sylas": "53.87%", - "Zilean": "53.51%", - "Pyke": "53.5%", - "Xerath": "52.75%", - "Nami": "52.72%" - }, + "champion_name": "Heimerdinger", + "pick_rate": "0.5%", + "counters": "no info", "synergy": { - "Karthus": "59.46%", - "Twitch": "53.57%", - "Xayah": "52.17%", - "Ashe": "51.05%", - "Varus": "50.88%", - "Tristana": "50.67%", + "Tristana": "80.00%", + "Aphelios": "80.00%", + "Draven": "64.29%", + "Twitch": "62.50%", + "Twisted Fate": "60.00%", + "Jhin": "55.56%", + "Varus": "53.85%", + "Senna": "51.28%", + "Miss Fortune": "50.00%", "Jinx": "50.00%", - "Zeri": "50.00%" + "Nilah": "50.00%" } }, { - "champion_name": "Brand", - "pick_rate": "2.0%", + "champion_name": "Ashe", + "pick_rate": "4.8%", "counters": { - "Sona": "58.11%", - "Maokai": "57.24%", - "Xerath": "57.03%", - "Janna": "56.31%", - "Pyke": "56.01%", - "Zyra": "55.13%", - "Leona": "54.96%", - "Zilean": "54.92%", - "Soraka": "54.73%", - "Senna": "54.19%" + "Camille": "58.72%", + "Maokai": "57.34%", + "Pyke": "56.14%", + "Blitzcrank": "55.88%", + "Nautilus": "55.46%", + "Taric": "55.41%", + "Vel'Koz": "54.55%", + "Zyra": "54.13%", + "Soraka": "53.87%", + "Xerath": "53.83%" }, "synergy": { - "Seraphine": "57.69%", - "Karthus": "55.17%", - "Yasuo": "55.17%", - "Kalista": "53.49%", - "Tristana": "52.31%", - "Xayah": "51.40%", - "Varus": "50.17%" + "Jinx": "53.73%", + "Jhin": "53.36%", + "Karthus": "52.31%", + "Kalista": "51.90%", + "Twisted Fate": "51.81%", + "Zeri": "50.65%" } }, { - "champion_name": "Seraphine", - "pick_rate": "2.0%", + "champion_name": "Sylas", + "pick_rate": "0.6%", "counters": { - "Sylas": "58.33%", - "Maokai": "57.24%", - "Zilean": "56.09%", - "Bard": "55.97%", - "Pyke": "54.69%", - "Soraka": "53.72%", - "Blitzcrank": "53.61%", - "Yuumi": "53.6%", - "Milio": "52.99%", - "Thresh": "52.66%" + "Taric": "70%", + "Shaco": "70%", + "Pantheon": "64.86%", + "Neeko": "63.16%", + "Thresh": "58.58%", + "Lulu": "58.52%", + "Morgana": "58.06%", + "Janna": "57.66%", + "Leona": "57.58%", + "Bard": "57.45%" }, "synergy": { - "Vayne": "59.62%", - "Tristana": "59.38%", - "Miss Fortune": "52.50%", - "Ashe": "52.20%", - "Jinx": "51.11%", - "Draven": "50.94%" + "Miss Fortune": "75.00%", + "Zeri": "72.73%", + "Twitch": "68.75%", + "Kalista": "68.42%", + "Draven": "65.22%", + "Tristana": "61.54%", + "Senna": "60.00%", + "Aphelios": "60.00%", + "Jinx": "55.56%", + "Jhin": "50.00%" } }, { - "champion_name": "Lulu", - "pick_rate": "7.4%", + "champion_name": "Neeko", + "pick_rate": "1.6%", "counters": { - "Taric": "56.84%", - "Zilean": "54.43%", - "Neeko": "54.04%", - "Senna": "54.04%", - "Sona": "53.78%", - "Zyra": "53.7%", - "Maokai": "53.46%", - "Blitzcrank": "52.96%", - "Bard": "52.89%", - "Thresh": "52.88%" - }, - "synergy": { - "Miss Fortune": "55.96%", - "Sivir": "55.62%", - "Tristana": "55.22%", - "Ashe": "51.59%", + "Zilean": "60.66%", + "Taric": "58.7%", + "Zac": "57.78%", + "Leona": "57.76%", + "Pantheon": "57.14%", + "Janna": "56.74%", + "Blitzcrank": "56.38%", + "Bard": "55.85%", + "Camille": "55.41%", + "Rell": "55.19%" + }, + "synergy": { + "Yasuo": "67.74%", + "Tristana": "66.67%", + "Twitch": "65.63%", + "Ashe": "60.87%", + "Senna": "59.22%", + "Nilah": "57.14%", + "Caitlyn": "54.55%", "Draven": "51.52%", - "Varus": "51.22%", - "Twitch": "51.14%", - "Jinx": "50.76%", - "Kog'Maw": "50.00%", - "Kalista": "50.00%" + "Twisted Fate": "50.94%", + "Vayne": "50.00%", + "Jinx": "50.00%", + "Miss Fortune": "50.00%" } }, { - "champion_name": "Morgana", - "pick_rate": "3.2%", - "counters": { - "Zyra": "56.73%", - "Senna": "56.35%", - "Rell": "56.29%", - "Janna": "55.6%", - "Milio": "55.2%", - "Vel'Koz": "55.08%", - "Nami": "54.67%", - "Taric": "54.31%", - "Karma": "54.24%", - "Maokai": "53.2%" - }, + "champion_name": "Karma", + "pick_rate": "9.6%", + "counters": "no info", "synergy": { - "Karthus": "55.56%", - "Nilah": "53.13%", - "Miss Fortune": "52.07%", - "Hwei": "51.72%", - "Vayne": "51.31%", - "Sivir": "50.91%", - "Draven": "50.63%", - "Tristana": "50.00%" + "Tristana": "54.41%", + "Ashe": "52.79%", + "Twisted Fate": "52.57%", + "Jinx": "52.21%", + "Miss Fortune": "51.67%", + "Kalista": "51.02%", + "Twitch": "50.19%", + "Caitlyn": "50.15%", + "Zeri": "50.00%" } }, { - "champion_name": "Karma", - "pick_rate": "7.6%", - "counters": { - "Pyke": "57.3%", - "Maokai": "55.33%", - "Zyra": "54.97%", - "Blitzcrank": "54.92%", - "Rakan": "54.52%", - "Janna": "54.36%", - "Sona": "54.33%", - "Nautilus": "54.21%", - "Soraka": "53.61%", - "Neeko": "52.83%" - }, - "synergy": { - "Draven": "53.57%", - "Vayne": "51.30%", - "Jhin": "50.49%", - "Ezreal": "50.43%", - "Tristana": "50.42%" + "champion_name": "Lux", + "pick_rate": "3.9%", + "counters": { + "Camille": "62.93%", + "Pantheon": "60%", + "Pyke": "58.92%", + "Bard": "57.24%", + "Shaco": "56.3%", + "Blitzcrank": "55.42%", + "Maokai": "55.39%", + "Leona": "55.08%", + "Alistar": "54.69%", + "Zilean": "54.61%" + }, + "synergy": { + "Ziggs": "57.14%", + "Twisted Fate": "56.82%", + "Samira": "52.63%", + "Senna": "52.05%", + "Ezreal": "50.65%", + "Nilah": "50.00%" } }, { - "champion_name": "Swain", - "pick_rate": "1.6%", - "counters": { - "Zilean": "57.77%", - "Sona": "57.32%", - "Vel'Koz": "57.25%", - "Janna": "56.41%", - "Brand": "55.87%", - "Yuumi": "55.78%", - "Zyra": "55.78%", - "Xerath": "55.48%", - "Soraka": "55.34%", - "Senna": "55.32%" - }, + "champion_name": "Seraphine", + "pick_rate": "1.7%", + "counters": "no info", "synergy": { - "Tristana": "64.29%", - "Karthus": "54.55%", - "Vayne": "54.01%", - "Jhin": "53.18%", - "Twitch": "50.70%", - "Nilah": "50.00%", - "Ziggs": "50.00%" + "Lucian": "61.54%", + "Tristana": "61.54%", + "Vayne": "58.90%", + "Varus": "57.69%", + "Jinx": "56.25%", + "Miss Fortune": "55.32%", + "Senna": "51.51%", + "Ezreal": "50.83%", + "Samira": "50.00%", + "Karthus": "50.00%" } }, { - "champion_name": "Pantheon", - "pick_rate": "1.0%", - "counters": { - "Neeko": "59.74%", - "Maokai": "58.43%", - "Karma": "57.22%", - "Bard": "57.14%", - "Swain": "56.25%", - "Rakan": "55.92%", - "Brand": "55.83%", - "Soraka": "55.74%", - "Sona": "55.56%", - "Janna": "55.42%" - }, - "synergy": { - "Miss Fortune": "63.41%", - "Seraphine": "56.25%", - "Yasuo": "54.55%", - "Swain": "52.63%", - "Varus": "52.05%", - "Zeri": "50.00%" + "champion_name": "Yuumi", + "pick_rate": "4.4%", + "counters": { + "Rell": "62.65%", + "Braum": "58.82%", + "Blitzcrank": "58.05%", + "Leona": "57.77%", + "Pyke": "57.61%", + "Taric": "57.46%", + "Swain": "56.85%", + "Rakan": "56.72%", + "Maokai": "56.12%", + "Janna": "55.85%" + }, + "synergy": { + "Rammus": "64.71%", + "Jinx": "55.32%", + "Vayne": "54.26%", + "Samira": "53.57%", + "Lucian": "53.52%", + "Tristana": "53.33%", + "Twitch": "53.31%", + "Jhin": "50.00%", + "Xayah": "50.00%" } }, { - "champion_name": "Yuumi", - "pick_rate": "4.9%", - "counters": { - "Maokai": "61.28%", - "Rell": "59.85%", - "Taric": "57.47%", - "Alistar": "57.31%", - "Nautilus": "56.64%", - "Rakan": "55.78%", - "Pyke": "55.71%", - "Leona": "55.63%", - "Sylas": "55.27%", - "Thresh": "55.23%" - }, + "champion_name": "Twitch", + "pick_rate": "0.5%", + "counters": "no info", "synergy": { - "Seraphine": "66.67%", - "Yasuo": "53.25%", - "Tristana": "52.55%", - "Vayne": "51.74%", - "Zeri": "51.45%", - "Twitch": "50.41%" + "Karma": "75.00%", + "Lucian": "66.67%", + "Cassiopeia": "66.67%", + "Janna": "66.67%", + "Aphelios": "66.67%", + "Jhin": "61.90%", + "Zeri": "60.87%", + "Varus": "60.00%", + "Kalista": "57.14%", + "Xayah": "53.85%", + "Ezreal": "53.52%", + "Vayne": "53.19%", + "Sivir": "50.00%", + "Jinx": "50.00%", + "Lulu": "50.00%" } }, { "champion_name": "Hwei", - "pick_rate": "2.1%", - "counters": { - "Taric": "66.91%", - "Sona": "66.55%", - "Blitzcrank": "65.66%", - "Janna": "65.3%", - "Brand": "65.18%", - "Pyke": "63.85%", - "Xerath": "63.02%", - "Vel'Koz": "62.71%", - "Morgana": "62.31%", - "Milio": "62.01%" - }, - "synergy": { - "Nilah": "51.61%" + "pick_rate": "2.9%", + "counters": { + "Camille": "63.76%", + "Pyke": "60.47%", + "Maokai": "58.72%", + "Xerath": "58.7%", + "Rakan": "58.47%", + "Taric": "58.43%", + "Swain": "58.1%", + "Zilean": "57.69%", + "Vel'Koz": "57.69%", + "Janna": "57.32%" + }, + "synergy": { + "Lucian": "57.14%", + "Tristana": "54.55%", + "Swain": "53.85%", + "Miss Fortune": "51.85%", + "Seraphine": "51.02%" } } ] \ No newline at end of file diff --git a/src/LoL-support/how-to-start.md b/src/LoL-support/how-to-start.md old mode 100644 new mode 100755 diff --git a/src/LoL-support/lolchamps.sh b/src/LoL-support/lolchamps.sh index 441b3b1..3f4b309 100755 --- a/src/LoL-support/lolchamps.sh +++ b/src/LoL-support/lolchamps.sh @@ -1,35 +1,37 @@ #!/bin/zsh -#python step1-lolchamps.py -#echo "Step 1 complete: Scraped targeted data from URL" +python step1-lolchamps.py echo "Step 1 Scrape targeted data from URL." -echo "TEMPORALY DISABLED, Data already extracted" +#echo "TEMPORALY DISABLED, Data already extracted" +#TBD: show the date when was the last time it scraped, python step2-lolchamps.py echo "Step 2 complete: extracted Champion names and their Pick rates" +#TBD: Winrates from diamond+ python step3-lolchamps.py echo "Step 3 complete: made data two-dimensional with Pandas, just for fun" -#python step4-lolchamps.py +python step4-lolchamps.py #enable only if list of champion names are lost echo "Step 4 complete: saved champion names in a list" -echo "TEMPORALY DISABLED, champ names already saved" +#echo "TEMPORALY DISABLED, champ names already saved" -#python step5-lolchamps.py +python step5-lolchamps.py echo "Step 5 in Beta mode: trying to iterate through champion names and scrape their counters." -echo "DISABLED BECAUSE COUNTERS ARE ALREADY SCRAPED AND SAVED" +#echo "DISABLED BECAUSE COUNTERS ARE ALREADY SCRAPED AND SAVED" #scraped only best counters. but script is unable to click for 'more counters' -#python step6-lolchamps.py +python step6-lolchamps.py echo "Step 6 in Beta mode: collecting synergies for each support champion" -echo "DISABLED BECAUSE SYNERGIES ARE ALREADY SCRAPED AND SAVED" +#echo "DISABLED BECAUSE SYNERGIES ARE ALREADY SCRAPED AND SAVED" #EUW and KR versions. Atm EUW works only. Currently it only shows DUO synergy, but I want ADC-SUPP-JUNGLER Synergy -#python step7-lolchamps.py +python step7-lolchamps.py echo "Step 7 complete: combining data into final_data" -#python step8-db.py +python step8-db.py echo "Step 8 in beta mode: sending final_data to Postgres Server tables" +#last extraction: 2024/03/04 chmod +x lolchamps.sh \ No newline at end of file diff --git a/src/LoL-support/pick_rates.json b/src/LoL-support/pick_rates.json old mode 100644 new mode 100755 index b8e55ed..f9a9a85 --- a/src/LoL-support/pick_rates.json +++ b/src/LoL-support/pick_rates.json @@ -1,170 +1,170 @@ [ { - "champion_name": "Taric", - "pick_rate": "1.4%" + "champion_name": "Maokai", + "pick_rate": "12.7%" }, { "champion_name": "Janna", - "pick_rate": "9.6%" + "pick_rate": "10.7%" }, { - "champion_name": "Maokai", - "pick_rate": "1.6%" + "champion_name": "Taric", + "pick_rate": "1.5%" }, { - "champion_name": "Bard", - "pick_rate": "6.1%" + "champion_name": "Braum", + "pick_rate": "3.8%" }, { - "champion_name": "Rakan", - "pick_rate": "12.8%" + "champion_name": "Blitzcrank", + "pick_rate": "8.1%" }, { - "champion_name": "Sona", - "pick_rate": "2.4%" + "champion_name": "Camille", + "pick_rate": "2.3%" }, { - "champion_name": "Senna", - "pick_rate": "11.0%" + "champion_name": "Leona", + "pick_rate": "4.4%" }, { "champion_name": "Rell", - "pick_rate": "4.9%" - }, - { - "champion_name": "Neeko", - "pick_rate": "1.9%" + "pick_rate": "4.1%" }, { - "champion_name": "Soraka", - "pick_rate": "5.5%" + "champion_name": "Rakan", + "pick_rate": "7.7%" }, { - "champion_name": "Blitzcrank", - "pick_rate": "8.8%" + "champion_name": "Zilean", + "pick_rate": "2.8%" }, { - "champion_name": "Zyra", - "pick_rate": "3.8%" + "champion_name": "Pyke", + "pick_rate": "8.5%" }, { - "champion_name": "Pyke", - "pick_rate": "7.6%" + "champion_name": "Zac", + "pick_rate": "1.0%" }, { - "champion_name": "Zilean", - "pick_rate": "2.5%" + "champion_name": "Alistar", + "pick_rate": "4.7%" }, { - "champion_name": "Braum", - "pick_rate": "3.6%" + "champion_name": "Bard", + "pick_rate": "5.7%" }, { - "champion_name": "Renata Glasc", - "pick_rate": "5.0%" + "champion_name": "Soraka", + "pick_rate": "5.4%" }, { - "champion_name": "Vel'Koz", - "pick_rate": "1.6%" + "champion_name": "Sona", + "pick_rate": "2.3%" }, { "champion_name": "Thresh", - "pick_rate": "13.3%" + "pick_rate": "9.4%" }, { - "champion_name": "Milio", - "pick_rate": "6.7%" + "champion_name": "Renata Glasc", + "pick_rate": "2.4%" }, { - "champion_name": "Alistar", - "pick_rate": "5.8%" + "champion_name": "Zyra", + "pick_rate": "4.3%" }, { - "champion_name": "Zac", - "pick_rate": "0.7%" + "champion_name": "Vel'Koz", + "pick_rate": "1.4%" }, { - "champion_name": "Leona", - "pick_rate": "4.8%" + "champion_name": "Senna", + "pick_rate": "11.0%" }, { - "champion_name": "Xerath", - "pick_rate": "4.8%" + "champion_name": "Shaco", + "pick_rate": "2.6%" }, { - "champion_name": "Nami", - "pick_rate": "6.5%" + "champion_name": "Pantheon", + "pick_rate": "1.8%" }, { - "champion_name": "Shaco", + "champion_name": "Tahm Kench", "pick_rate": "1.1%" }, { - "champion_name": "Heimerdinger", - "pick_rate": "1.0%" + "champion_name": "Nautilus", + "pick_rate": "11.0%" }, { - "champion_name": "Sylas", - "pick_rate": "1.0%" + "champion_name": "Milio", + "pick_rate": "8.1%" }, { - "champion_name": "Tahm Kench", - "pick_rate": "0.6%" + "champion_name": "Nami", + "pick_rate": "5.5%" }, { - "champion_name": "Amumu", - "pick_rate": "0.6%" + "champion_name": "Xerath", + "pick_rate": "2.7%" }, { - "champion_name": "Twitch", - "pick_rate": "0.8%" + "champion_name": "Brand", + "pick_rate": "1.9%" }, { - "champion_name": "Nautilus", - "pick_rate": "10.9%" + "champion_name": "Swain", + "pick_rate": "1.4%" }, { - "champion_name": "Ashe", - "pick_rate": "5.7%" + "champion_name": "Lulu", + "pick_rate": "7.9%" }, { - "champion_name": "Lux", - "pick_rate": "5.1%" + "champion_name": "Morgana", + "pick_rate": "2.8%" }, { - "champion_name": "Brand", - "pick_rate": "2.0%" + "champion_name": "Heimerdinger", + "pick_rate": "0.5%" }, { - "champion_name": "Seraphine", - "pick_rate": "2.0%" + "champion_name": "Ashe", + "pick_rate": "4.8%" }, { - "champion_name": "Lulu", - "pick_rate": "7.4%" + "champion_name": "Sylas", + "pick_rate": "0.6%" }, { - "champion_name": "Morgana", - "pick_rate": "3.2%" + "champion_name": "Neeko", + "pick_rate": "1.6%" }, { "champion_name": "Karma", - "pick_rate": "7.6%" + "pick_rate": "9.6%" }, { - "champion_name": "Swain", - "pick_rate": "1.6%" + "champion_name": "Lux", + "pick_rate": "3.9%" }, { - "champion_name": "Pantheon", - "pick_rate": "1.0%" + "champion_name": "Seraphine", + "pick_rate": "1.7%" }, { "champion_name": "Yuumi", - "pick_rate": "4.9%" + "pick_rate": "4.4%" + }, + { + "champion_name": "Twitch", + "pick_rate": "0.5%" }, { "champion_name": "Hwei", - "pick_rate": "2.1%" + "pick_rate": "2.9%" } ] \ No newline at end of file diff --git a/src/LoL-support/sorted_final_data.json b/src/LoL-support/sorted_final_data.json old mode 100644 new mode 100755 index 7517365..1a22c5a --- a/src/LoL-support/sorted_final_data.json +++ b/src/LoL-support/sorted_final_data.json @@ -1,1233 +1,934 @@ [ { - "champion_name": "Thresh", - "pick_rate": "13.3%", - "counters": { - "Taric": "53.39%", - "Janna": "52.43%", - "Brand": "52.12%", - "Rakan": "52.02%", - "Neeko": "51.75%", - "Sona": "51.47%", - "Maokai": "51.34%", - "Vel'Koz": "51.27%", - "Bard": "51.24%", - "Renata Glasc": "51.24%" - }, - "synergy": { - "Tristana": "55.29%", - "Hwei": "54.01%", - "Draven": "53.28%", - "Jinx": "52.84%", - "Aphelios": "51.39%", - "Twitch": "51.28%", - "Kalista": "51.17%", - "Samira": "51.08%", - "Vayne": "51.01%", - "Karthus": "50.91%", - "Lucian": "50.61%", - "Ashe": "50.26%", - "Yasuo": "50.24%" - } - }, - { - "champion_name": "Rakan", - "pick_rate": "12.8%", - "counters": { - "Senna": "52.31%", - "Bard": "51.42%", - "Soraka": "51.41%", - "Neeko": "51.37%", - "Renata Glasc": "51.09%", - "Sona": "50.93%", - "Maokai": "50.87%", - "Rell": "50.69%", - "Zilean": "50.64%", - "Janna": "50.58%" + "champion_name": "Maokai", + "pick_rate": "12.7%", + "counters": { + "Taric": "50.48%", + "Alistar": "49.83%", + "Braum": "49.49%", + "Leona": "49.02%", + "Janna": "48.98%", + "Renata Glasc": "48.48%", + "Rell": "47.97%", + "Shaco": "47.18%", + "Camille": "47.04%", + "Zyra": "47.02%" }, - "synergy": { - "Lucian": "56.02%", - "Jinx": "54.48%", - "Twitch": "54.35%", - "Seraphine": "54.17%", - "Ziggs": "54.08%", - "Vayne": "54.03%", - "Sivir": "53.73%", - "Jhin": "52.40%", - "Yasuo": "51.97%", - "Karthus": "51.90%", - "Ezreal": "51.77%", - "Ashe": "51.55%", - "Miss Fortune": "50.65%", - "Nilah": "50.61%", - "Varus": "50.35%", - "Draven": "50.12%" - } + "synergy": [ + "no data" + ] }, { "champion_name": "Senna", "pick_rate": "11.0%", - "counters": { - "Pyke": "52.31%", - "Blitzcrank": "52.25%", - "Xerath": "51.66%", - "Maokai": "51.62%", - "Zyra": "51.1%", - "Taric": "50.76%", - "Leona": "50.22%", - "Bard": "50.06%", - "Lux": "49.9%", - "Rell": "49.8%" - }, - "synergy": { - "Kalista": "57.48%", - "Nilah": "56.48%", - "Karthus": "56.18%", - "Zeri": "55.11%", - "Seraphine": "55.06%", - "Miss Fortune": "54.98%", - "Cho'Gath": "54.69%", - "Vayne": "54.60%", - "Ashe": "54.54%", - "Tahm Kench": "54.51%", - "Yasuo": "54.49%", - "Brand": "54.26%", - "Twitch": "54.15%", - "Swain": "53.87%", - "Jhin": "52.87%", - "Ziggs": "52.59%", - "Sivir": "52.21%", - "Ezreal": "51.36%", - "Tristana": "51.36%", - "Varus": "50.75%" - } + "counters": "no info", + "synergy": [ + "no data" + ] }, { "champion_name": "Nautilus", - "pick_rate": "10.9%", + "pick_rate": "11.0%", "counters": { - "Taric": "55.98%", - "Rell": "55.66%", - "Renata Glasc": "55.13%", - "Sylas": "55.09%", - "Braum": "54.08%", - "Rakan": "54.06%", - "Vel'Koz": "53.45%", - "Swain": "53.09%", - "Neeko": "52.97%", - "Alistar": "52.87%" - }, - "synergy": { - "Seraphine": "58.68%", - "Ziggs": "55.43%", - "Karthus": "55.21%", - "Lucian": "51.71%", - "Tristana": "51.50%", - "Samira": "51.14%", - "Kalista": "50.71%", - "Jinx": "50.13%" + "Taric": "60.99%", + "Leona": "57.54%", + "Braum": "56.73%", + "Renata Glasc": "55.96%", + "Alistar": "55.65%", + "Rell": "55.38%", + "Camille": "54.74%", + "Pantheon": "54.53%", + "Swain": "54.35%", + "Maokai": "53.72%" + }, + "synergy": { + "Seraphine": "55.21%", + "Senna": "54.59%", + "Kalista": "52.55%", + "Xayah": "52.25%", + "Caitlyn": "51.40%", + "Karthus": "51.11%", + "Twitch": "50.96%", + "Lucian": "50.86%", + "Jhin": "50.64%", + "Draven": "50.62%", + "Vayne": "50.27%", + "Zeri": "50.21%", + "Samira": "50.09%", + "Nilah": "50.00%" } }, { "champion_name": "Janna", - "pick_rate": "9.6%", - "counters": { - "Senna": "50.79%", - "Sona": "50.1%", - "Soraka": "50.01%", - "Seraphine": "49.87%", - "Milio": "49.71%", - "Neeko": "49.67%", - "Rell": "49.5%", - "Bard": "49.44%", - "Rakan": "49.42%", - "Blitzcrank": "49.4%" - }, - "synergy": { - "Seraphine": "58.66%", - "Vayne": "55.43%", - "Yasuo": "54.91%", - "Ziggs": "54.85%", - "Jinx": "53.96%", - "Ashe": "53.85%", - "Tristana": "53.63%", - "Kalista": "53.47%", - "Kog'Maw": "52.89%", - "Karthus": "52.83%", - "Jhin": "52.65%", - "Ezreal": "52.58%", - "Lucian": "52.51%", - "Draven": "52.25%", - "Twitch": "52.00%", - "Samira": "51.88%", - "Varus": "51.73%", - "Zeri": "51.64%", - "Caitlyn": "51.53%", - "Kai'Sa": "50.95%" + "pick_rate": "10.7%", + "counters": { + "Camille": "53.25%", + "Sona": "51.89%", + "Blitzcrank": "51.2%", + "Maokai": "51.02%", + "Braum": "49.67%", + "Pyke": "49.38%", + "Renata Glasc": "49.3%", + "Senna": "49.25%", + "Ashe": "49.07%", + "Nami": "48.64%" + }, + "synergy": { + "Nilah": "57.38%", + "Twitch": "57.02%", + "Senna": "56.68%", + "Seraphine": "55.59%", + "Miss Fortune": "54.71%", + "Zeri": "54.30%", + "Tristana": "54.22%", + "Draven": "54.22%", + "Twisted Fate": "53.57%", + "Samira": "53.57%", + "Jhin": "53.03%", + "Lucian": "52.84%", + "Karthus": "52.73%", + "Ezreal": "52.60%", + "Yasuo": "52.58%", + "Kalista": "52.35%", + "Vayne": "51.97%", + "Ashe": "51.80%", + "Sivir": "51.10%", + "Caitlyn": "51.05%", + "Smolder": "50.23%", + "Aphelios": "50.23%" } }, { - "champion_name": "Blitzcrank", - "pick_rate": "8.8%", - "counters": { - "Taric": "56.85%", - "Rakan": "52.88%", - "Leona": "52.47%", - "Braum": "52.28%", - "Renata Glasc": "51.05%", - "Maokai": "51.04%", - "Zyra": "50.98%", - "Rell": "50.78%", - "Alistar": "50.73%", - "Zilean": "50.7%" - }, + "champion_name": "Karma", + "pick_rate": "9.6%", + "counters": "no info", "synergy": { - "Nilah": "58.59%", - "Xayah": "55.50%", - "Draven": "54.17%", - "Lucian": "53.85%", - "Ashe": "53.42%", - "Vayne": "52.91%", - "Twitch": "52.80%", - "Seraphine": "52.34%", - "Jinx": "51.90%", - "Karthus": "51.89%", - "Varus": "51.87%", - "Tristana": "51.72%", - "Miss Fortune": "51.53%", - "Zeri": "51.27%", - "Aphelios": "50.95%", - "Kalista": "50.84%", - "Jhin": "50.74%", - "Ezreal": "50.08%", - "Caitlyn": "50.00%" + "Tristana": "54.41%", + "Ashe": "52.79%", + "Twisted Fate": "52.57%", + "Jinx": "52.21%", + "Miss Fortune": "51.67%", + "Kalista": "51.02%", + "Twitch": "50.19%", + "Caitlyn": "50.15%", + "Zeri": "50.00%" } }, { - "champion_name": "Pyke", - "pick_rate": "7.6%", - "counters": { - "Rakan": "53.38%", - "Maokai": "52.78%", - "Neeko": "52.1%", - "Rell": "52.08%", - "Taric": "51.82%", - "Janna": "51.79%", - "Blitzcrank": "51.44%", - "Alistar": "50.94%", - "Bard": "50.94%", - "Thresh": "50.49%" - }, + "champion_name": "Thresh", + "pick_rate": "9.4%", + "counters": "no info", "synergy": { - "Karthus": "61.98%", - "Nilah": "60.43%", - "Yasuo": "57.30%", - "Tristana": "55.96%", - "Twitch": "55.08%", - "Ashe": "54.27%", - "Swain": "53.91%", - "Ziggs": "53.28%", - "Seraphine": "52.82%", - "Vayne": "52.64%", - "Varus": "51.58%", - "Jhin": "51.51%", - "Ezreal": "51.49%", - "Hwei": "51.43%", - "Draven": "50.95%" + "Yasuo": "62.82%", + "Senna": "58.33%", + "Xayah": "56.52%", + "Sivir": "56.25%", + "Seraphine": "55.74%", + "Kalista": "54.41%", + "Twitch": "52.68%", + "Miss Fortune": "52.44%", + "Jinx": "52.43%", + "Draven": "52.37%", + "Aphelios": "52.12%", + "Smolder": "51.38%", + "Tristana": "51.11%", + "Samira": "50.59%", + "Varus": "50.32%", + "Vayne": "50.15%", + "Kai'Sa": "50.05%" } }, { - "champion_name": "Karma", - "pick_rate": "7.6%", - "counters": { - "Pyke": "57.3%", - "Maokai": "55.33%", - "Zyra": "54.97%", - "Blitzcrank": "54.92%", - "Rakan": "54.52%", - "Janna": "54.36%", - "Sona": "54.33%", - "Nautilus": "54.21%", - "Soraka": "53.61%", - "Neeko": "52.83%" - }, + "champion_name": "Pyke", + "pick_rate": "8.5%", + "counters": "no info", "synergy": { - "Draven": "53.57%", - "Vayne": "51.30%", - "Jhin": "50.49%", - "Ezreal": "50.43%", - "Tristana": "50.42%" + "Karthus": "60.47%", + "Swain": "58.06%", + "Senna": "57.89%", + "Ashe": "55.02%", + "Twitch": "54.55%", + "Vayne": "53.90%", + "Jinx": "53.67%", + "Yasuo": "53.66%", + "Seraphine": "53.19%", + "Twisted Fate": "53.07%", + "Nilah": "53.00%", + "Smolder": "51.63%", + "Zeri": "50.56%", + "Ezreal": "50.22%", + "Draven": "50.00%" } }, { - "champion_name": "Lulu", - "pick_rate": "7.4%", - "counters": { - "Taric": "56.84%", - "Zilean": "54.43%", - "Neeko": "54.04%", - "Senna": "54.04%", - "Sona": "53.78%", - "Zyra": "53.7%", - "Maokai": "53.46%", - "Blitzcrank": "52.96%", - "Bard": "52.89%", - "Thresh": "52.88%" - }, - "synergy": { - "Miss Fortune": "55.96%", - "Sivir": "55.62%", - "Tristana": "55.22%", - "Ashe": "51.59%", - "Draven": "51.52%", - "Varus": "51.22%", - "Twitch": "51.14%", - "Jinx": "50.76%", - "Kog'Maw": "50.00%", - "Kalista": "50.00%" + "champion_name": "Blitzcrank", + "pick_rate": "8.1%", + "counters": { + "Pantheon": "54.85%", + "Leona": "54.08%", + "Maokai": "53.66%", + "Braum": "53.61%", + "Rakan": "53.26%", + "Taric": "53.13%", + "Alistar": "52.33%", + "Shaco": "51.5%", + "Rell": "50.9%", + "Zilean": "50.57%" + }, + "synergy": { + "Seraphine": "65.22%", + "Karthus": "57.78%", + "Draven": "56.57%", + "Vayne": "55.39%", + "Varus": "54.74%", + "Jinx": "54.30%", + "Twisted Fate": "54.17%", + "Tristana": "53.18%", + "Sivir": "53.06%", + "Senna": "52.85%", + "Aphelios": "52.14%", + "Kai'Sa": "51.65%", + "Lucian": "50.78%", + "Nilah": "50.67%", + "Twitch": "50.00%", + "Ashe": "50.00%", + "Zeri": "50.00%" } }, { "champion_name": "Milio", - "pick_rate": "6.7%", - "counters": { - "Taric": "54.79%", - "Blitzcrank": "53.66%", - "Braum": "53.22%", - "Vel'Koz": "53.08%", - "Senna": "52.78%", - "Bard": "52.75%", - "Neeko": "52.17%", - "Rakan": "51.91%", - "Soraka": "51.52%", - "Thresh": "51.33%" + "pick_rate": "8.1%", + "counters": { + "Blitzcrank": "56.32%", + "Braum": "55.45%", + "Maokai": "54.37%", + "Camille": "53.86%", + "Janna": "53.43%", + "Thresh": "53.37%", + "Zilean": "53.04%", + "Tahm Kench": "53.04%", + "Vel'Koz": "52.98%", + "Leona": "52.96%" }, - "synergy": { - "Kog'Maw": "53.82%", - "Xayah": "52.76%", - "Lucian": "51.94%", - "Zeri": "51.56%", - "Vayne": "51.56%", - "Kalista": "51.56%", - "Ashe": "51.48%", - "Draven": "50.69%", - "Aphelios": "50.42%", - "Twitch": "50.00%" - } - }, - { - "champion_name": "Nami", - "pick_rate": "6.5%", - "counters": { - "Maokai": "56.37%", - "Neeko": "53.14%", - "Blitzcrank": "53.03%", - "Rakan": "52.58%", - "Zyra": "52.29%", - "Soraka": "51.92%", - "Milio": "51.67%", - "Pyke": "51.56%", - "Janna": "51.44%", - "Bard": "51.33%" - }, - "synergy": { - "Swain": "59.52%", - "Brand": "55.69%", - "Karthus": "55.65%", - "Twitch": "52.16%", - "Ashe": "51.88%", - "Seraphine": "51.43%", - "Jhin": "51.32%", - "Vayne": "51.29%", - "Ezreal": "51.27%" - } + "synergy": [ + "no data" + ] }, { - "champion_name": "Bard", - "pick_rate": "6.1%", - "counters": { - "Taric": "52.2%", - "Janna": "50.56%", - "Maokai": "50.28%", - "Zilean": "49.95%", - "Senna": "49.94%", - "Blitzcrank": "49.89%", - "Renata Glasc": "49.52%", - "Soraka": "49.45%", - "Rell": "49.3%", - "Pyke": "49.06%" + "champion_name": "Lulu", + "pick_rate": "7.9%", + "counters": { + "Vel'Koz": "57.35%", + "Blitzcrank": "56.23%", + "Zilean": "55.78%", + "Tahm Kench": "55.43%", + "Renata Glasc": "54.86%", + "Rell": "54.7%", + "Janna": "54.6%", + "Maokai": "54.51%", + "Braum": "53.79%", + "Ashe": "53.65%" }, - "synergy": { - "Nilah": "59.77%", - "Vayne": "55.15%", - "Caitlyn": "55.12%", - "Twitch": "55.06%", - "Ziggs": "54.37%", - "Tristana": "53.92%", - "Karthus": "53.62%", - "Seraphine": "53.05%", - "Lucian": "52.97%", - "Ezreal": "52.51%", - "Ashe": "52.44%", - "Jhin": "51.89%", - "Swain": "51.69%", - "Jinx": "51.34%", - "Zeri": "50.76%" - } + "synergy": [ + "no data" + ] }, { - "champion_name": "Alistar", - "pick_rate": "5.8%", - "counters": { - "Sona": "54.51%", - "Janna": "54.04%", - "Swain": "53.24%", - "Vel'Koz": "52.78%", - "Senna": "52.64%", - "Zilean": "52.39%", - "Soraka": "52.22%", - "Seraphine": "51.97%", - "Xerath": "51.86%", - "Milio": "51.85%" - }, - "synergy": { - "Seraphine": "59.43%", - "Ziggs": "57.86%", - "Sivir": "55.20%", - "Hwei": "54.65%", - "Zeri": "53.37%", - "Ashe": "52.55%", - "Yasuo": "52.42%", - "Vayne": "52.01%", - "Twitch": "51.83%", - "Xayah": "51.49%", - "Jinx": "50.82%", - "Jhin": "50.68%", - "Ezreal": "50.58%", - "Draven": "50.28%", - "Miss Fortune": "50.20%" - } + "champion_name": "Rakan", + "pick_rate": "7.7%", + "counters": "no info", + "synergy": [ + "no data" + ] }, { - "champion_name": "Ashe", + "champion_name": "Bard", "pick_rate": "5.7%", "counters": { - "Pyke": "56.98%", - "Sylas": "54.95%", - "Blitzcrank": "54.83%", - "Heimerdinger": "54.8%", - "Rell": "54.67%", - "Nautilus": "54.35%", - "Bard": "53.44%", - "Milio": "53.1%", - "Rakan": "52.69%", - "Brand": "52.5%" - }, - "synergy": { - "Karthus": "56.83%", - "Seraphine": "54.60%", - "Jinx": "54.17%", - "Nilah": "53.85%", - "Twitch": "53.55%", - "Ezreal": "51.69%", - "Swain": "51.32%", - "Ziggs": "50.79%", - "Lucian": "50.00%", - "Zeri": "50.00%" + "Maokai": "56.1%", + "Renata Glasc": "53.77%", + "Janna": "53.45%", + "Pyke": "51.79%", + "Morgana": "51.04%", + "Nami": "50.92%", + "Pantheon": "50.72%", + "Blitzcrank": "50.43%", + "Brand": "50.34%", + "Senna": "50.21%" + }, + "synergy": { + "Yasuo": "60.34%", + "Seraphine": "59.53%", + "Senna": "59.42%", + "Ziggs": "57.58%", + "Caitlyn": "55.43%", + "Xayah": "54.26%", + "Hwei": "54.17%", + "Twisted Fate": "53.66%", + "Jinx": "52.43%", + "Twitch": "51.68%", + "Aphelios": "51.54%", + "Karthus": "51.47%", + "Samira": "50.60%", + "Vayne": "50.34%" } }, { - "champion_name": "Soraka", + "champion_name": "Nami", "pick_rate": "5.5%", "counters": { - "Blitzcrank": "54.17%", - "Neeko": "51.31%", - "Sona": "51.25%", - "Zyra": "51.13%", - "Vel'Koz": "51.05%", - "Bard": "50.55%", - "Senna": "50.38%", - "Yuumi": "50.06%", - "Janna": "49.99%", - "Pyke": "49.98%" + "Morgana": "56.08%", + "Blitzcrank": "55.61%", + "Maokai": "55.57%", + "Vel'Koz": "54.47%", + "Rakan": "54.44%", + "Neeko": "54.21%", + "Rell": "53.9%", + "Nautilus": "53.32%", + "Thresh": "53.28%", + "Senna": "53.17%" }, - "synergy": { - "Yasuo": "63.54%", - "Swain": "59.70%", - "Samira": "58.41%", - "Vayne": "57.03%", - "Ashe": "53.73%", - "Jinx": "53.47%", - "Ziggs": "53.38%", - "Jhin": "53.17%", - "Tristana": "52.80%", - "Lucian": "52.34%", - "Miss Fortune": "51.96%", - "Twitch": "51.63%", - "Draven": "50.30%" - } - }, - { - "champion_name": "Lux", - "pick_rate": "5.1%", - "counters": { - "Blitzcrank": "56.57%", - "Maokai": "54.94%", - "Zyra": "54.66%", - "Shaco": "54.45%", - "Vel'Koz": "54.1%", - "Sylas": "53.87%", - "Zilean": "53.51%", - "Pyke": "53.5%", - "Xerath": "52.75%", - "Nami": "52.72%" - }, - "synergy": { - "Karthus": "59.46%", - "Twitch": "53.57%", - "Xayah": "52.17%", - "Ashe": "51.05%", - "Varus": "50.88%", - "Tristana": "50.67%", - "Jinx": "50.00%", - "Zeri": "50.00%" - } + "synergy": [ + "no data" + ] }, { - "champion_name": "Renata Glasc", - "pick_rate": "5.0%", + "champion_name": "Soraka", + "pick_rate": "5.4%", "counters": "no info", "synergy": [ "no data" ] }, { - "champion_name": "Rell", - "pick_rate": "4.9%", + "champion_name": "Ashe", + "pick_rate": "4.8%", "counters": { - "Poppy": "53.79%", - "Seraphine": "53.57%", - "Soraka": "52.57%", - "Zilean": "52.54%", - "Alistar": "52.45%", - "Shaco": "51.5%", - "Maokai": "50.89%", - "Sona": "50.75%", - "Bard": "50.7%", - "Janna": "50.5%" + "Camille": "58.72%", + "Maokai": "57.34%", + "Pyke": "56.14%", + "Blitzcrank": "55.88%", + "Nautilus": "55.46%", + "Taric": "55.41%", + "Vel'Koz": "54.55%", + "Zyra": "54.13%", + "Soraka": "53.87%", + "Xerath": "53.83%" }, "synergy": { - "Ziggs": "61.31%", - "Karthus": "60.18%", - "Seraphine": "59.02%", - "Tristana": "58.16%", - "Lucian": "57.30%", - "Twitch": "56.30%", - "Miss Fortune": "54.44%", - "Ashe": "53.88%", - "Sivir": "53.49%", - "Jinx": "52.83%", - "Draven": "52.25%", - "Xayah": "52.10%", - "Ezreal": "52.08%", - "Kai'Sa": "51.33%", - "Samira": "51.15%", - "Zeri": "50.35%", - "Yasuo": "50.25%", - "Aphelios": "50.21%" + "Jinx": "53.73%", + "Jhin": "53.36%", + "Karthus": "52.31%", + "Kalista": "51.90%", + "Twisted Fate": "51.81%", + "Zeri": "50.65%" } }, { - "champion_name": "Yuumi", - "pick_rate": "4.9%", - "counters": { - "Maokai": "61.28%", - "Rell": "59.85%", - "Taric": "57.47%", - "Alistar": "57.31%", - "Nautilus": "56.64%", - "Rakan": "55.78%", - "Pyke": "55.71%", - "Leona": "55.63%", - "Sylas": "55.27%", - "Thresh": "55.23%" - }, + "champion_name": "Alistar", + "pick_rate": "4.7%", + "counters": "no info", "synergy": { - "Seraphine": "66.67%", - "Yasuo": "53.25%", - "Tristana": "52.55%", - "Vayne": "51.74%", - "Zeri": "51.45%", - "Twitch": "50.41%" + "Karthus": "64.86%", + "Seraphine": "62.86%", + "Zeri": "60.83%", + "Sivir": "59.26%", + "Jinx": "58.42%", + "Yasuo": "58.09%", + "Tristana": "55.84%", + "Senna": "53.27%", + "Twitch": "52.99%", + "Draven": "51.74%", + "Xayah": "51.25%", + "Ezreal": "50.91%", + "Twisted Fate": "50.75%", + "Samira": "50.00%" } }, { "champion_name": "Leona", - "pick_rate": "4.8%", - "counters": { - "Taric": "56.7%", - "Janna": "54.1%", - "Bard": "54.03%", - "Shaco": "53.67%", - "Maokai": "53.55%", - "Rell": "53.22%", - "Soraka": "53.11%", - "Lux": "53.09%", - "Morgana": "53.02%", - "Braum": "51.72%" - }, + "pick_rate": "4.4%", + "counters": "no info", "synergy": { - "Seraphine": "56.47%", - "Xayah": "55.00%", - "Zeri": "54.63%", - "Ezreal": "54.35%", - "Twitch": "54.27%", - "Caitlyn": "53.68%", - "Jhin": "53.21%", - "Yasuo": "53.19%", - "Lucian": "52.72%", - "Miss Fortune": "51.92%", - "Aphelios": "51.70%", - "Tristana": "51.48%", - "Kalista": "51.16%" + "Swain": "68.09%", + "Senna": "60.41%", + "Jinx": "59.02%", + "Seraphine": "58.90%", + "Ashe": "57.67%", + "Aphelios": "56.67%", + "Nilah": "56.52%", + "Vayne": "55.20%", + "Lucian": "54.63%", + "Smolder": "54.42%", + "Miss Fortune": "53.39%", + "Karthus": "53.06%", + "Xayah": "51.61%", + "Ezreal": "51.04%", + "Kalista": "50.59%", + "Twisted Fate": "50.31%" } }, { - "champion_name": "Xerath", - "pick_rate": "4.8%", - "counters": { - "Sylas": "55%", - "Maokai": "53.98%", - "Sona": "53.77%", - "Janna": "53.15%", - "Pyke": "52.84%", - "Soraka": "52.33%", - "Rakan": "52.02%", - "Leona": "51.76%", - "Zyra": "51.72%", - "Nami": "51.6%" - }, - "synergy": { - "Ziggs": "55.24%", - "Jinx": "53.95%", - "Vayne": "53.32%", - "Samira": "52.63%", - "Xayah": "52.57%", - "Ashe": "52.33%", - "Jhin": "51.81%", - "Caitlyn": "51.12%", - "Hwei": "50.88%", - "Tristana": "50.70%", - "Ezreal": "50.00%" + "champion_name": "Yuumi", + "pick_rate": "4.4%", + "counters": { + "Rell": "62.65%", + "Braum": "58.82%", + "Blitzcrank": "58.05%", + "Leona": "57.77%", + "Pyke": "57.61%", + "Taric": "57.46%", + "Swain": "56.85%", + "Rakan": "56.72%", + "Maokai": "56.12%", + "Janna": "55.85%" + }, + "synergy": { + "Rammus": "64.71%", + "Jinx": "55.32%", + "Vayne": "54.26%", + "Samira": "53.57%", + "Lucian": "53.52%", + "Tristana": "53.33%", + "Twitch": "53.31%", + "Jhin": "50.00%", + "Xayah": "50.00%" } }, { "champion_name": "Zyra", - "pick_rate": "3.8%", - "counters": { - "Sona": "53.56%", - "Pyke": "53.25%", - "Heimerdinger": "53.11%", - "Bard": "53.07%", - "Leona": "51.91%", - "Janna": "51.88%", - "Shaco": "51.56%", - "Taric": "51.43%", - "Rakan": "50.98%", - "Nautilus": "50.75%" - }, - "synergy": { - "Seraphine": "55.06%", - "Vayne": "54.84%", - "Tristana": "54.05%", - "Twitch": "52.96%", - "Samira": "51.98%", - "Jhin": "51.51%", - "Karthus": "51.16%", - "Draven": "50.65%", - "Xayah": "50.00%" + "pick_rate": "4.3%", + "counters": { + "Taric": "54.73%", + "Leona": "54.05%", + "Sona": "53.31%", + "Maokai": "52.98%", + "Renata Glasc": "52.84%", + "Pyke": "52.6%", + "Zac": "52.59%", + "Janna": "52.42%", + "Zilean": "51.88%", + "Yuumi": "51.63%" + }, + "synergy": { + "Xayah": "66.07%", + "Sivir": "61.54%", + "Twisted Fate": "53.68%", + "Miss Fortune": "53.25%", + "Ezreal": "52.52%", + "Senna": "52.28%", + "Tristana": "50.94%", + "Vayne": "50.18%", + "Jinx": "50.00%" } }, { - "champion_name": "Braum", - "pick_rate": "3.6%", - "counters": { - "Zilean": "55.95%", - "Soraka": "54.2%", - "Bard": "53.76%", - "Rell": "53.74%", - "Taric": "53.44%", - "Sona": "53.2%", - "Senna": "53.06%", - "Rakan": "52.88%", - "Janna": "52.49%", - "Renata Glasc": "52.31%" - }, + "champion_name": "Rell", + "pick_rate": "4.1%", + "counters": "no info", "synergy": { - "Sivir": "60.82%", - "Twitch": "59.55%", - "Xayah": "54.40%", - "Varus": "52.69%", - "Kog'Maw": "52.03%", - "Ashe": "51.92%", - "Jinx": "51.32%", - "Tristana": "50.94%", - "Vayne": "50.83%", - "Draven": "50.75%", - "Ezreal": "50.35%" + "Xayah": "58.33%", + "Twisted Fate": "56.72%", + "Twitch": "56.48%", + "Nilah": "56.25%", + "Miss Fortune": "55.88%", + "Zeri": "55.45%", + "Senna": "55.08%", + "Vayne": "53.65%", + "Jinx": "53.40%", + "Kalista": "52.46%", + "Samira": "52.46%", + "Aphelios": "52.17%", + "Draven": "52.15%", + "Tristana": "51.90%", + "Karthus": "51.43%", + "Ezreal": "51.32%", + "Jhin": "50.43%", + "Yasuo": "50.00%" } }, { - "champion_name": "Morgana", - "pick_rate": "3.2%", + "champion_name": "Lux", + "pick_rate": "3.9%", + "counters": { + "Camille": "62.93%", + "Pantheon": "60%", + "Pyke": "58.92%", + "Bard": "57.24%", + "Shaco": "56.3%", + "Blitzcrank": "55.42%", + "Maokai": "55.39%", + "Leona": "55.08%", + "Alistar": "54.69%", + "Zilean": "54.61%" + }, + "synergy": { + "Ziggs": "57.14%", + "Twisted Fate": "56.82%", + "Samira": "52.63%", + "Senna": "52.05%", + "Ezreal": "50.65%", + "Nilah": "50.00%" + } + }, + { + "champion_name": "Braum", + "pick_rate": "3.8%", + "counters": "no info", + "synergy": [ + "no data" + ] + }, + { + "champion_name": "Hwei", + "pick_rate": "2.9%", "counters": { - "Zyra": "56.73%", - "Senna": "56.35%", - "Rell": "56.29%", - "Janna": "55.6%", - "Milio": "55.2%", - "Vel'Koz": "55.08%", - "Nami": "54.67%", - "Taric": "54.31%", - "Karma": "54.24%", - "Maokai": "53.2%" + "Camille": "63.76%", + "Pyke": "60.47%", + "Maokai": "58.72%", + "Xerath": "58.7%", + "Rakan": "58.47%", + "Taric": "58.43%", + "Swain": "58.1%", + "Zilean": "57.69%", + "Vel'Koz": "57.69%", + "Janna": "57.32%" }, "synergy": { - "Karthus": "55.56%", - "Nilah": "53.13%", - "Miss Fortune": "52.07%", - "Hwei": "51.72%", - "Vayne": "51.31%", - "Sivir": "50.91%", - "Draven": "50.63%", - "Tristana": "50.00%" + "Lucian": "57.14%", + "Tristana": "54.55%", + "Swain": "53.85%", + "Miss Fortune": "51.85%", + "Seraphine": "51.02%" } }, { "champion_name": "Zilean", - "pick_rate": "2.5%", - "counters": { - "Janna": "53.27%", - "Shaco": "53.02%", - "Senna": "51.99%", - "Neeko": "51.67%", - "Maokai": "51.41%", - "Xerath": "51.07%", - "Milio": "50.67%", - "Sylas": "50.34%", - "Soraka": "50.28%", - "Vel'Koz": "50.23%" - }, + "pick_rate": "2.8%", + "counters": "no info", "synergy": { - "Swain": "58.97%", - "Hwei": "55.56%", - "Samira": "54.12%", - "Kog'Maw": "54.05%", - "Twitch": "53.80%", - "Aphelios": "53.47%", - "Vayne": "51.88%", - "Caitlyn": "51.79%", - "Seraphine": "51.67%", - "Ezreal": "51.07%", - "Ashe": "50.12%" + "Miss Fortune": "62.03%", + "Twisted Fate": "61.05%", + "Draven": "54.63%", + "Swain": "54.55%", + "Vayne": "54.47%", + "Ashe": "53.97%", + "Caitlyn": "53.13%", + "Seraphine": "52.44%", + "Smolder": "51.37%", + "Xayah": "51.22%", + "Sivir": "50.00%" } }, { - "champion_name": "Sona", - "pick_rate": "2.4%", - "counters": { - "Taric": "56.83%", - "Leona": "55.04%", - "Seraphine": "53.49%", - "Blitzcrank": "52.56%", - "Senna": "51.71%", - "Bard": "51.4%", - "Nami": "50.59%", - "Milio": "50.43%", - "Zilean": "50.26%", - "Pyke": "50.19%" - }, + "champion_name": "Morgana", + "pick_rate": "2.8%", + "counters": "no info", "synergy": { - "Yasuo": "64.52%", - "Tristana": "60.29%", - "Ziggs": "58.51%", - "Samira": "57.45%", - "Sivir": "56.60%", - "Vayne": "56.55%", - "Nilah": "55.88%", - "Kog'Maw": "53.49%", - "Lucian": "53.04%", - "Xayah": "52.90%", - "Seraphine": "52.76%", - "Ezreal": "52.71%", - "Miss Fortune": "52.23%", - "Varus": "51.05%", - "Ashe": "50.93%", - "Caitlyn": "50.81%", - "Twitch": "50.21%" + "Xayah": "60.00%", + "Draven": "57.81%", + "Nilah": "54.55%", + "Samira": "51.61%", + "Sivir": "50.00%", + "Seraphine": "50.00%" } }, { - "champion_name": "Hwei", - "pick_rate": "2.1%", - "counters": { - "Taric": "66.91%", - "Sona": "66.55%", - "Blitzcrank": "65.66%", - "Janna": "65.3%", - "Brand": "65.18%", - "Pyke": "63.85%", - "Xerath": "63.02%", - "Vel'Koz": "62.71%", - "Morgana": "62.31%", - "Milio": "62.01%" - }, - "synergy": { - "Nilah": "51.61%" + "champion_name": "Xerath", + "pick_rate": "2.7%", + "counters": { + "Blitzcrank": "58.63%", + "Neeko": "56.43%", + "Sona": "56.36%", + "Maokai": "56.26%", + "Pyke": "56.15%", + "Leona": "56.02%", + "Braum": "55.06%", + "Rakan": "54.83%", + "Nautilus": "54.65%", + "Janna": "53.86%" + }, + "synergy": { + "Samira": "70.00%", + "Twisted Fate": "61.36%", + "Caitlyn": "57.97%", + "Karthus": "55.56%", + "Ashe": "52.46%", + "Vayne": "50.55%", + "Zeri": "50.00%" } }, { - "champion_name": "Brand", - "pick_rate": "2.0%", - "counters": { - "Sona": "58.11%", - "Maokai": "57.24%", - "Xerath": "57.03%", - "Janna": "56.31%", - "Pyke": "56.01%", - "Zyra": "55.13%", - "Leona": "54.96%", - "Zilean": "54.92%", - "Soraka": "54.73%", - "Senna": "54.19%" - }, + "champion_name": "Shaco", + "pick_rate": "2.6%", + "counters": "no info", "synergy": { - "Seraphine": "57.69%", - "Karthus": "55.17%", - "Yasuo": "55.17%", - "Kalista": "53.49%", - "Tristana": "52.31%", - "Xayah": "51.40%", - "Varus": "50.17%" + "Karthus": "76.92%", + "Twisted Fate": "62.64%", + "Tristana": "58.62%", + "Senna": "56.19%", + "Miss Fortune": "56.10%", + "Aphelios": "52.78%", + "Hwei": "52.63%", + "Ashe": "52.48%", + "Draven": "52.24%", + "Nilah": "50.00%", + "Caitlyn": "50.00%", + "Ziggs": "50.00%" } }, { - "champion_name": "Seraphine", - "pick_rate": "2.0%", + "champion_name": "Renata Glasc", + "pick_rate": "2.4%", + "counters": "no info", + "synergy": [ + "no data" + ] + }, + { + "champion_name": "Camille", + "pick_rate": "2.3%", "counters": { - "Sylas": "58.33%", - "Maokai": "57.24%", - "Zilean": "56.09%", - "Bard": "55.97%", - "Pyke": "54.69%", - "Soraka": "53.72%", - "Blitzcrank": "53.61%", - "Yuumi": "53.6%", - "Milio": "52.99%", - "Thresh": "52.66%" - }, - "synergy": { - "Vayne": "59.62%", - "Tristana": "59.38%", - "Miss Fortune": "52.50%", - "Ashe": "52.20%", - "Jinx": "51.11%", - "Draven": "50.94%" + "Taric": "58.76%", + "Renata Glasc": "58.12%", + "Braum": "57.32%", + "Leona": "54.48%", + "Brand": "54.2%", + "Bard": "53.76%", + "Zac": "53.03%", + "Maokai": "52.96%", + "Tahm Kench": "52.94%", + "Zilean": "52.78%" + }, + "synergy": { + "Twitch": "62.67%", + "Sivir": "58.33%", + "Zeri": "56.47%", + "Tristana": "56.20%", + "Senna": "55.81%", + "Xayah": "55.26%", + "Aphelios": "53.62%", + "Nilah": "53.13%", + "Seraphine": "52.94%", + "Varus": "52.89%", + "Karthus": "52.63%", + "Ashe": "52.50%", + "Smolder": "52.34%", + "Jhin": "52.09%", + "Draven": "52.06%", + "Ziggs": "52.00%", + "Twisted Fate": "50.94%", + "Vayne": "50.92%", + "Lucian": "50.35%", + "Swain": "50.00%" } }, { - "champion_name": "Neeko", + "champion_name": "Sona", + "pick_rate": "2.3%", + "counters": { + "Zilean": "61.68%", + "Rell": "59.62%", + "Leona": "57.21%", + "Neeko": "55.71%", + "Thresh": "55.03%", + "Maokai": "54.7%", + "Bard": "53.59%", + "Braum": "51.81%", + "Blitzcrank": "51.79%", + "Seraphine": "51.2%" + }, + "synergy": { + "Nilah": "71.43%", + "Tristana": "63.16%", + "Twisted Fate": "56.19%", + "Jhin": "55.34%", + "Kai'Sa": "55.11%", + "Varus": "52.63%", + "Aphelios": "52.00%", + "Twitch": "51.46%", + "Vayne": "51.22%", + "Smolder": "50.72%", + "Lucian": "50.00%", + "Samira": "50.00%" + } + }, + { + "champion_name": "Brand", "pick_rate": "1.9%", - "counters": { - "Vel'Koz": "60.45%", - "Sona": "53.26%", - "Bard": "52.61%", - "Brand": "52.38%", - "Taric": "52.17%", - "Alistar": "51%", - "Shaco": "50.68%", - "Senna": "50.42%", - "Renata Glasc": "50.38%", - "Morgana": "50.35%" - }, + "counters": "no info", "synergy": { - "Nilah": "77.78%", - "Yasuo": "59.46%", - "Xayah": "59.26%", - "Hwei": "58.82%", - "Lucian": "56.79%", - "Vayne": "53.85%", - "Twitch": "53.17%", - "Kalista": "50.75%", - "Ezreal": "50.72%", - "Ashe": "50.29%", - "Quinn": "50.00%", - "Samira": "50.00%", - "Varus": "50.00%" + "Samira": "59.09%", + "Yasuo": "57.89%", + "Twisted Fate": "57.14%", + "Xayah": "56.52%", + "Jinx": "55.88%", + "Jhin": "53.98%", + "Draven": "53.70%", + "Seraphine": "52.50%", + "Aphelios": "50.00%" } }, { - "champion_name": "Maokai", - "pick_rate": "1.6%", - "counters": { - "Shaco": "58.18%", - "Braum": "54.8%", - "Sona": "53.99%", - "Janna": "52.72%", - "Renata Glasc": "52.39%", - "Zyra": "51.46%", - "Alistar": "51.45%", - "Soraka": "50.8%", - "Taric": "50.42%", - "Neeko": "50.29%" - }, + "champion_name": "Pantheon", + "pick_rate": "1.8%", + "counters": "no info", "synergy": { - "Xayah": "60.00%", - "Ziggs": "58.33%", - "Karthus": "57.89%", - "Kai'Sa": "53.95%", - "Vayne": "53.51%", - "Sivir": "52.63%", - "Jhin": "52.46%", - "Aphelios": "52.46%", - "Draven": "52.38%", - "Ezreal": "52.29%", - "Samira": "51.81%", - "Varus": "51.53%", - "Zeri": "51.02%", - "Twitch": "50.00%", - "Ashe": "50.00%", - "Hwei": "50.00%" + "Karthus": "66.67%", + "Twisted Fate": "58.82%", + "Miss Fortune": "56.60%", + "Senna": "56.59%", + "Jhin": "55.67%", + "Nilah": "54.55%", + "Ezreal": "54.37%", + "Swain": "53.85%", + "Lucian": "53.06%", + "Ashe": "52.38%", + "Smolder": "52.07%", + "Samira": "51.97%", + "Jinx": "51.72%" } }, { - "champion_name": "Vel'Koz", - "pick_rate": "1.6%", - "counters": { - "Pyke": "55.14%", - "Zyra": "54.76%", - "Seraphine": "54.15%", - "Blitzcrank": "53.89%", - "Janna": "53.62%", - "Bard": "53.22%", - "Sona": "52.97%", - "Nami": "52.1%", - "Maokai": "51.85%", - "Renata Glasc": "51.64%" - }, + "champion_name": "Seraphine", + "pick_rate": "1.7%", + "counters": "no info", "synergy": { - "Karthus": "61.90%", - "Vayne": "55.34%", - "Sivir": "54.72%", - "Ashe": "54.44%", - "Aphelios": "53.25%", - "Seraphine": "52.38%", - "Jhin": "51.92%", - "Caitlyn": "51.43%", - "Ezreal": "51.22%", - "Twitch": "50.39%", - "Varus": "50.37%", - "Tristana": "50.00%", - "Miss Fortune": "50.00%" + "Lucian": "61.54%", + "Tristana": "61.54%", + "Vayne": "58.90%", + "Varus": "57.69%", + "Jinx": "56.25%", + "Miss Fortune": "55.32%", + "Senna": "51.51%", + "Ezreal": "50.83%", + "Samira": "50.00%", + "Karthus": "50.00%" } }, { - "champion_name": "Swain", + "champion_name": "Neeko", "pick_rate": "1.6%", "counters": { - "Zilean": "57.77%", - "Sona": "57.32%", - "Vel'Koz": "57.25%", - "Janna": "56.41%", - "Brand": "55.87%", - "Yuumi": "55.78%", - "Zyra": "55.78%", - "Xerath": "55.48%", - "Soraka": "55.34%", - "Senna": "55.32%" - }, - "synergy": { - "Tristana": "64.29%", - "Karthus": "54.55%", - "Vayne": "54.01%", - "Jhin": "53.18%", - "Twitch": "50.70%", - "Nilah": "50.00%", - "Ziggs": "50.00%" + "Zilean": "60.66%", + "Taric": "58.7%", + "Zac": "57.78%", + "Leona": "57.76%", + "Pantheon": "57.14%", + "Janna": "56.74%", + "Blitzcrank": "56.38%", + "Bard": "55.85%", + "Camille": "55.41%", + "Rell": "55.19%" + }, + "synergy": { + "Yasuo": "67.74%", + "Tristana": "66.67%", + "Twitch": "65.63%", + "Ashe": "60.87%", + "Senna": "59.22%", + "Nilah": "57.14%", + "Caitlyn": "54.55%", + "Draven": "51.52%", + "Twisted Fate": "50.94%", + "Vayne": "50.00%", + "Jinx": "50.00%", + "Miss Fortune": "50.00%" } }, { "champion_name": "Taric", - "pick_rate": "1.4%", + "pick_rate": "1.5%", "counters": { - "Soraka": "55.26%", - "Rell": "52.91%", - "Vel'Koz": "52.75%", - "Janna": "50.88%", - "Zilean": "50.55%", - "Rakan": "49.62%", - "Maokai": "49.58%", - "Nami": "49.42%", - "Xerath": "49.28%", - "Senna": "49.24%" - }, - "synergy": { - "Karthus": "70.83%", - "Yasuo": "68.91%", - "Lee Sin": "68.18%", - "Master Yi": "59.26%", - "Kog'Maw": "58.82%", - "Sivir": "58.62%", - "Varus": "56.63%", - "Swain": "56.45%", - "Seraphine": "56.41%", - "Kalista": "55.94%", - "Ziggs": "55.00%", - "Vayne": "54.01%", - "Jinx": "52.38%", - "Lucian": "51.91%", - "Kai'Sa": "51.81%", - "Ashe": "51.67%", - "Miss Fortune": "50.94%", - "Ezreal": "50.20%", - "Twitch": "50.00%" + "Swain": "57.63%", + "Sona": "57.32%", + "Janna": "54.88%", + "Nami": "54.27%", + "Lux": "53.54%", + "Seraphine": "53.45%", + "Bard": "52.97%", + "Soraka": "52.32%", + "Thresh": "52.21%", + "Braum": "51.67%" + }, + "synergy": { + "Vayne": "60.16%", + "Ashe": "58.46%", + "Yasuo": "58.33%", + "Twisted Fate": "57.89%", + "Pyke": "57.14%", + "Senna": "56.59%", + "Nilah": "56.32%", + "Kalista": "55.13%", + "Jinx": "54.39%", + "Aphelios": "53.57%", + "Seraphine": "53.13%", + "Zeri": "52.54%", + "Twitch": "52.05%", + "Smolder": "51.89%", + "Ezreal": "51.67%", + "Kai'Sa": "50.59%", + "Sivir": "50.00%", + "Lucian": "50.00%", + "Dr. Mundo": "50.00%", + "Xayah": "50.00%", + "Karthus": "50.00%" } }, { - "champion_name": "Shaco", - "pick_rate": "1.1%", - "counters": { - "Sona": "60.95%", - "Swain": "60.22%", - "Taric": "59.68%", - "Heimerdinger": "59.02%", - "Zac": "56.14%", - "Janna": "55.96%", - "Milio": "54.49%", - "Pyke": "54.23%", - "Rakan": "53.2%", - "Bard": "52.59%" - }, + "champion_name": "Vel'Koz", + "pick_rate": "1.4%", + "counters": "no info", "synergy": { - "Seraphine": "85.71%", - "Miss Fortune": "58.33%", - "Samira": "54.84%", - "Caitlyn": "54.43%", - "Jhin": "54.01%", - "Varus": "52.43%", + "Samira": "65.00%", + "Nilah": "60.00%", + "Hwei": "58.82%", + "Seraphine": "54.55%", + "Draven": "53.66%", + "Ashe": "53.52%", + "Tristana": "53.33%", + "Zeri": "53.13%", + "Senna": "51.90%", + "Kai'Sa": "51.02%", + "Jhin": "50.71%", + "Smolder": "50.22%", + "Sivir": "50.00%", "Karthus": "50.00%" } }, { - "champion_name": "Heimerdinger", - "pick_rate": "1.0%", + "champion_name": "Swain", + "pick_rate": "1.4%", "counters": { - "Maokai": "61.64%", - "Neeko": "56.82%", - "Seraphine": "55.95%", - "Xerath": "54.86%", - "Bard": "54.55%", - "Leona": "54.4%", - "Rell": "53.15%", - "Rakan": "52.67%", - "Lulu": "52.56%", - "Janna": "52.56%" + "Zac": "58.97%", + "Sona": "58.67%", + "Neeko": "57.41%", + "Nami": "57.14%", + "Milio": "56.44%", + "Pantheon": "55.95%", + "Zyra": "55.49%", + "Morgana": "55.24%", + "Leona": "55%", + "Camille": "54.79%" }, - "synergy": { - "Zeri": "68.00%", - "Sivir": "57.89%", - "Draven": "54.29%", - "Jhin": "54.03%", - "Vayne": "53.49%", - "Miss Fortune": "52.78%", - "Ashe": "50.34%", - "Kalista": "50.00%", - "Twitch": "50.00%" - } + "synergy": [ + "no data" + ] }, { - "champion_name": "Sylas", - "pick_rate": "1.0%", - "counters": { - "Sona": "60%", - "Janna": "57.91%", - "Vel'Koz": "57.75%", - "Braum": "57.52%", - "Morgana": "57.5%", - "Taric": "55.56%", - "Rell": "54.55%", - "Rakan": "53.89%", - "Blitzcrank": "53.74%", - "Thresh": "53.64%" - }, + "champion_name": "Tahm Kench", + "pick_rate": "1.1%", + "counters": "no info", "synergy": { - "Karthus": "60.00%", - "Aphelios": "58.06%", - "Nilah": "57.89%", - "Miss Fortune": "57.14%", - "Kalista": "55.36%", - "Lucian": "54.65%", - "Seraphine": "54.55%", - "Sivir": "52.94%", - "Zeri": "52.63%", - "Vayne": "52.53%", - "Jhin": "52.38%", - "Ezreal": "51.14%", - "Twitch": "50.77%", - "Yasuo": "50.00%" + "Kalista": "68.75%", + "Jinx": "65.00%", + "Nilah": "62.50%", + "Xayah": "62.50%", + "Twitch": "57.14%", + "Varus": "55.10%", + "Twisted Fate": "54.55%", + "Zeri": "53.85%", + "Senna": "52.51%", + "Seraphine": "50.00%" } }, { - "champion_name": "Pantheon", + "champion_name": "Zac", "pick_rate": "1.0%", "counters": { - "Neeko": "59.74%", - "Maokai": "58.43%", - "Karma": "57.22%", - "Bard": "57.14%", - "Swain": "56.25%", - "Rakan": "55.92%", - "Brand": "55.83%", - "Soraka": "55.74%", - "Sona": "55.56%", - "Janna": "55.42%" - }, - "synergy": { - "Miss Fortune": "63.41%", + "Pantheon": "61.36%", + "Taric": "58.82%", + "Zilean": "58.49%", + "Rell": "56.82%", + "Bard": "56.55%", + "Morgana": "55.93%", + "Janna": "54.44%", + "Maokai": "53.44%", + "Milio": "53.25%", + "Thresh": "53.18%" + }, + "synergy": { + "Aphelios": "83.33%", + "Caitlyn": "66.67%", + "Draven": "65.52%", + "Senna": "64.71%", + "Tristana": "61.54%", + "Twisted Fate": "61.29%", + "Zeri": "58.82%", "Seraphine": "56.25%", - "Yasuo": "54.55%", - "Swain": "52.63%", - "Varus": "52.05%", - "Zeri": "50.00%" - } - }, - { - "champion_name": "Twitch", - "pick_rate": "0.8%", - "counters": { - "Zilean": "61.97%", - "Taric": "60.47%", - "Leona": "57.92%", - "Soraka": "57.87%", - "Shaco": "57.38%", - "Nami": "57.14%", - "Rakan": "56.34%", - "Sylas": "55.56%", - "Brand": "55.26%", - "Pyke": "54.65%" - }, - "synergy": { - "Sivir": "70.97%", - "Swain": "70.00%", - "Seraphine": "63.64%", - "Draven": "57.89%", - "Cassiopeia": "53.85%", - "Vayne": "53.54%", - "Jhin": "53.40%", - "Jinx": "52.27%", - "Ziggs": "51.72%", - "Zeri": "51.61%", - "Nilah": "50.00%", - "Kalista": "50.00%", - "Hwei": "50.00%", - "Tristana": "50.00%" + "Jinx": "55.00%", + "Ezreal": "52.38%", + "Smolder": "51.58%", + "Kalista": "51.52%", + "Xayah": "50.00%" } }, { - "champion_name": "Zac", - "pick_rate": "0.7%", + "champion_name": "Sylas", + "pick_rate": "0.6%", "counters": { - "Poppy": "67.39%", - "Maokai": "57.14%", - "Neeko": "56.72%", - "Swain": "56%", - "Vel'Koz": "54.1%", - "Taric": "53.85%", - "Alistar": "53.74%", - "Ashe": "53.49%", - "Rakan": "53.42%", - "Zilean": "53.26%" + "Taric": "70%", + "Shaco": "70%", + "Pantheon": "64.86%", + "Neeko": "63.16%", + "Thresh": "58.58%", + "Lulu": "58.52%", + "Morgana": "58.06%", + "Janna": "57.66%", + "Leona": "57.58%", + "Bard": "57.45%" }, "synergy": { - "Ziggs": "72.73%", - "Sivir": "71.43%", - "Zeri": "70.00%", - "Varus": "63.64%", - "Yasuo": "62.07%", - "Jhin": "59.32%", - "Kalista": "57.14%", - "Hwei": "57.14%", - "Twitch": "55.32%", - "Ezreal": "52.63%", - "Miss Fortune": "52.38%", - "Vayne": "50.75%", - "Swain": "50.00%", - "Tristana": "50.00%", - "Caitlyn": "50.00%" + "Miss Fortune": "75.00%", + "Zeri": "72.73%", + "Twitch": "68.75%", + "Kalista": "68.42%", + "Draven": "65.22%", + "Tristana": "61.54%", + "Senna": "60.00%", + "Aphelios": "60.00%", + "Jinx": "55.56%", + "Jhin": "50.00%" } }, { - "champion_name": "Tahm Kench", - "pick_rate": "0.6%", - "counters": { - "Taric": "62.22%", - "Braum": "61.11%", - "Bard": "57.4%", - "Zyra": "57.27%", - "Pyke": "55.38%", - "Maokai": "54.9%", - "Brand": "54.67%", - "Lulu": "54.55%", - "Renata Glasc": "54.17%", - "Rakan": "54.12%" - }, + "champion_name": "Heimerdinger", + "pick_rate": "0.5%", + "counters": "no info", "synergy": { - "Veigar": "80.00%", - "Ziggs": "75.00%", - "Karthus": "66.67%", - "Seraphine": "58.33%", - "Xayah": "58.33%", + "Tristana": "80.00%", + "Aphelios": "80.00%", + "Draven": "64.29%", + "Twitch": "62.50%", + "Twisted Fate": "60.00%", "Jhin": "55.56%", - "Miss Fortune": "51.72%", - "Twitch": "51.28%", - "Kog'Maw": "50.00%", - "Kalista": "50.00%", - "Ashe": "50.00%", - "Ezreal": "50.00%" + "Varus": "53.85%", + "Senna": "51.28%", + "Miss Fortune": "50.00%", + "Jinx": "50.00%", + "Nilah": "50.00%" } }, { - "champion_name": "Amumu", - "pick_rate": "0.6%", - "counters": { - "Maokai": "66.07%", - "Zyra": "61.68%", - "Rell": "58.9%", - "Rakan": "58.55%", - "Janna": "58.5%", - "Neeko": "57.14%", - "Taric": "56.1%", - "Braum": "55.41%", - "Bard": "54%", - "Leona": "53.91%" - }, + "champion_name": "Twitch", + "pick_rate": "0.5%", + "counters": "no info", "synergy": { - "Yasuo": "71.43%", - "Ziggs": "66.67%", - "Kalista": "64.15%", - "Lucian": "60.00%", - "Karthus": "60.00%", - "Hwei": "57.14%", - "Ezreal": "55.88%", - "Nilah": "55.56%", - "Draven": "55.41%", - "Miss Fortune": "54.37%", - "Ashe": "54.17%", - "Jinx": "53.85%", - "Kai'Sa": "53.60%", - "Tristana": "52.63%", - "Zeri": "50.00%", - "Veigar": "50.00%" + "Karma": "75.00%", + "Lucian": "66.67%", + "Cassiopeia": "66.67%", + "Janna": "66.67%", + "Aphelios": "66.67%", + "Jhin": "61.90%", + "Zeri": "60.87%", + "Varus": "60.00%", + "Kalista": "57.14%", + "Xayah": "53.85%", + "Ezreal": "53.52%", + "Vayne": "53.19%", + "Sivir": "50.00%", + "Jinx": "50.00%", + "Lulu": "50.00%" } } ] \ No newline at end of file diff --git a/src/LoL-support/step1-lolchamps.py b/src/LoL-support/step1-lolchamps.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/step2-lolchamps.py b/src/LoL-support/step2-lolchamps.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/step3-lolchamps.py b/src/LoL-support/step3-lolchamps.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/step4-lolchamps.py b/src/LoL-support/step4-lolchamps.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/step5-lolchamps.py b/src/LoL-support/step5-lolchamps.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/step6-lolchamps.py b/src/LoL-support/step6-lolchamps.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/step7-lolchamps.py b/src/LoL-support/step7-lolchamps.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/step8-db.py b/src/LoL-support/step8-db.py old mode 100644 new mode 100755 diff --git a/src/LoL-support/support_combined.json b/src/LoL-support/support_combined.json old mode 100644 new mode 100755 index 4e4f71b..3c678d9 --- a/src/LoL-support/support_combined.json +++ b/src/LoL-support/support_combined.json @@ -1,663 +1,443 @@ [ { - "champion_name": "Taric", - "pick_rate": "1.4%", + "champion_name": "Maokai", + "pick_rate": "12.7%", "counters": { - "Soraka": "55.26%", - "Rell": "52.91%", - "Vel'Koz": "52.75%", - "Janna": "50.88%", - "Zilean": "50.55%", - "Rakan": "49.62%", - "Maokai": "49.58%", - "Nami": "49.42%", - "Xerath": "49.28%", - "Senna": "49.24%" + "Taric": "50.48%", + "Alistar": "49.83%", + "Braum": "49.49%", + "Leona": "49.02%", + "Janna": "48.98%", + "Renata Glasc": "48.48%", + "Rell": "47.97%", + "Shaco": "47.18%", + "Camille": "47.04%", + "Zyra": "47.02%" } }, { "champion_name": "Janna", - "pick_rate": "9.6%", + "pick_rate": "10.7%", "counters": { - "Senna": "50.79%", - "Sona": "50.1%", - "Soraka": "50.01%", - "Seraphine": "49.87%", - "Milio": "49.71%", - "Neeko": "49.67%", - "Rell": "49.5%", - "Bard": "49.44%", - "Rakan": "49.42%", - "Blitzcrank": "49.4%" + "Camille": "53.25%", + "Sona": "51.89%", + "Blitzcrank": "51.2%", + "Maokai": "51.02%", + "Braum": "49.67%", + "Pyke": "49.38%", + "Renata Glasc": "49.3%", + "Senna": "49.25%", + "Ashe": "49.07%", + "Nami": "48.64%" } }, { - "champion_name": "Maokai", - "pick_rate": "1.6%", + "champion_name": "Taric", + "pick_rate": "1.5%", "counters": { - "Shaco": "58.18%", - "Braum": "54.8%", - "Sona": "53.99%", - "Janna": "52.72%", - "Renata Glasc": "52.39%", - "Zyra": "51.46%", - "Alistar": "51.45%", - "Soraka": "50.8%", - "Taric": "50.42%", - "Neeko": "50.29%" + "Swain": "57.63%", + "Sona": "57.32%", + "Janna": "54.88%", + "Nami": "54.27%", + "Lux": "53.54%", + "Seraphine": "53.45%", + "Bard": "52.97%", + "Soraka": "52.32%", + "Thresh": "52.21%", + "Braum": "51.67%" } }, { - "champion_name": "Bard", - "pick_rate": "6.1%", - "counters": { - "Taric": "52.2%", - "Janna": "50.56%", - "Maokai": "50.28%", - "Zilean": "49.95%", - "Senna": "49.94%", - "Blitzcrank": "49.89%", - "Renata Glasc": "49.52%", - "Soraka": "49.45%", - "Rell": "49.3%", - "Pyke": "49.06%" - } + "champion_name": "Braum", + "pick_rate": "3.8%", + "counters": "no info" }, { - "champion_name": "Rakan", - "pick_rate": "12.8%", - "counters": { - "Senna": "52.31%", - "Bard": "51.42%", - "Soraka": "51.41%", - "Neeko": "51.37%", - "Renata Glasc": "51.09%", - "Sona": "50.93%", - "Maokai": "50.87%", - "Rell": "50.69%", - "Zilean": "50.64%", - "Janna": "50.58%" + "champion_name": "Blitzcrank", + "pick_rate": "8.1%", + "counters": { + "Pantheon": "54.85%", + "Leona": "54.08%", + "Maokai": "53.66%", + "Braum": "53.61%", + "Rakan": "53.26%", + "Taric": "53.13%", + "Alistar": "52.33%", + "Shaco": "51.5%", + "Rell": "50.9%", + "Zilean": "50.57%" } }, { - "champion_name": "Sona", - "pick_rate": "2.4%", + "champion_name": "Camille", + "pick_rate": "2.3%", "counters": { - "Taric": "56.83%", - "Leona": "55.04%", - "Seraphine": "53.49%", - "Blitzcrank": "52.56%", - "Senna": "51.71%", - "Bard": "51.4%", - "Nami": "50.59%", - "Milio": "50.43%", - "Zilean": "50.26%", - "Pyke": "50.19%" + "Taric": "58.76%", + "Renata Glasc": "58.12%", + "Braum": "57.32%", + "Leona": "54.48%", + "Brand": "54.2%", + "Bard": "53.76%", + "Zac": "53.03%", + "Maokai": "52.96%", + "Tahm Kench": "52.94%", + "Zilean": "52.78%" } }, { - "champion_name": "Senna", - "pick_rate": "11.0%", - "counters": { - "Pyke": "52.31%", - "Blitzcrank": "52.25%", - "Xerath": "51.66%", - "Maokai": "51.62%", - "Zyra": "51.1%", - "Taric": "50.76%", - "Leona": "50.22%", - "Bard": "50.06%", - "Lux": "49.9%", - "Rell": "49.8%" - } + "champion_name": "Leona", + "pick_rate": "4.4%", + "counters": "no info" }, { "champion_name": "Rell", - "pick_rate": "4.9%", - "counters": { - "Poppy": "53.79%", - "Seraphine": "53.57%", - "Soraka": "52.57%", - "Zilean": "52.54%", - "Alistar": "52.45%", - "Shaco": "51.5%", - "Maokai": "50.89%", - "Sona": "50.75%", - "Bard": "50.7%", - "Janna": "50.5%" - } - }, - { - "champion_name": "Neeko", - "pick_rate": "1.9%", - "counters": { - "Vel'Koz": "60.45%", - "Sona": "53.26%", - "Bard": "52.61%", - "Brand": "52.38%", - "Taric": "52.17%", - "Alistar": "51%", - "Shaco": "50.68%", - "Senna": "50.42%", - "Renata Glasc": "50.38%", - "Morgana": "50.35%" - } + "pick_rate": "4.1%", + "counters": "no info" }, { - "champion_name": "Soraka", - "pick_rate": "5.5%", - "counters": { - "Blitzcrank": "54.17%", - "Neeko": "51.31%", - "Sona": "51.25%", - "Zyra": "51.13%", - "Vel'Koz": "51.05%", - "Bard": "50.55%", - "Senna": "50.38%", - "Yuumi": "50.06%", - "Janna": "49.99%", - "Pyke": "49.98%" - } + "champion_name": "Rakan", + "pick_rate": "7.7%", + "counters": "no info" }, { - "champion_name": "Blitzcrank", - "pick_rate": "8.8%", - "counters": { - "Taric": "56.85%", - "Rakan": "52.88%", - "Leona": "52.47%", - "Braum": "52.28%", - "Renata Glasc": "51.05%", - "Maokai": "51.04%", - "Zyra": "50.98%", - "Rell": "50.78%", - "Alistar": "50.73%", - "Zilean": "50.7%" - } + "champion_name": "Zilean", + "pick_rate": "2.8%", + "counters": "no info" }, { - "champion_name": "Zyra", - "pick_rate": "3.8%", - "counters": { - "Sona": "53.56%", - "Pyke": "53.25%", - "Heimerdinger": "53.11%", - "Bard": "53.07%", - "Leona": "51.91%", - "Janna": "51.88%", - "Shaco": "51.56%", - "Taric": "51.43%", - "Rakan": "50.98%", - "Nautilus": "50.75%" - } + "champion_name": "Pyke", + "pick_rate": "8.5%", + "counters": "no info" }, { - "champion_name": "Pyke", - "pick_rate": "7.6%", + "champion_name": "Zac", + "pick_rate": "1.0%", "counters": { - "Rakan": "53.38%", - "Maokai": "52.78%", - "Neeko": "52.1%", - "Rell": "52.08%", - "Taric": "51.82%", - "Janna": "51.79%", - "Blitzcrank": "51.44%", - "Alistar": "50.94%", - "Bard": "50.94%", - "Thresh": "50.49%" + "Pantheon": "61.36%", + "Taric": "58.82%", + "Zilean": "58.49%", + "Rell": "56.82%", + "Bard": "56.55%", + "Morgana": "55.93%", + "Janna": "54.44%", + "Maokai": "53.44%", + "Milio": "53.25%", + "Thresh": "53.18%" } }, { - "champion_name": "Zilean", - "pick_rate": "2.5%", - "counters": { - "Janna": "53.27%", - "Shaco": "53.02%", - "Senna": "51.99%", - "Neeko": "51.67%", - "Maokai": "51.41%", - "Xerath": "51.07%", - "Milio": "50.67%", - "Sylas": "50.34%", - "Soraka": "50.28%", - "Vel'Koz": "50.23%" - } + "champion_name": "Alistar", + "pick_rate": "4.7%", + "counters": "no info" }, { - "champion_name": "Braum", - "pick_rate": "3.6%", + "champion_name": "Bard", + "pick_rate": "5.7%", "counters": { - "Zilean": "55.95%", - "Soraka": "54.2%", - "Bard": "53.76%", - "Rell": "53.74%", - "Taric": "53.44%", - "Sona": "53.2%", - "Senna": "53.06%", - "Rakan": "52.88%", - "Janna": "52.49%", - "Renata Glasc": "52.31%" + "Maokai": "56.1%", + "Renata Glasc": "53.77%", + "Janna": "53.45%", + "Pyke": "51.79%", + "Morgana": "51.04%", + "Nami": "50.92%", + "Pantheon": "50.72%", + "Blitzcrank": "50.43%", + "Brand": "50.34%", + "Senna": "50.21%" } }, { - "champion_name": "Renata Glasc", - "pick_rate": "5.0%", + "champion_name": "Soraka", + "pick_rate": "5.4%", "counters": "no info" }, { - "champion_name": "Vel'Koz", - "pick_rate": "1.6%", + "champion_name": "Sona", + "pick_rate": "2.3%", "counters": { - "Pyke": "55.14%", - "Zyra": "54.76%", - "Seraphine": "54.15%", - "Blitzcrank": "53.89%", - "Janna": "53.62%", - "Bard": "53.22%", - "Sona": "52.97%", - "Nami": "52.1%", - "Maokai": "51.85%", - "Renata Glasc": "51.64%" + "Zilean": "61.68%", + "Rell": "59.62%", + "Leona": "57.21%", + "Neeko": "55.71%", + "Thresh": "55.03%", + "Maokai": "54.7%", + "Bard": "53.59%", + "Braum": "51.81%", + "Blitzcrank": "51.79%", + "Seraphine": "51.2%" } }, { "champion_name": "Thresh", - "pick_rate": "13.3%", - "counters": { - "Taric": "53.39%", - "Janna": "52.43%", - "Brand": "52.12%", - "Rakan": "52.02%", - "Neeko": "51.75%", - "Sona": "51.47%", - "Maokai": "51.34%", - "Vel'Koz": "51.27%", - "Bard": "51.24%", - "Renata Glasc": "51.24%" - } + "pick_rate": "9.4%", + "counters": "no info" }, { - "champion_name": "Milio", - "pick_rate": "6.7%", - "counters": { - "Taric": "54.79%", - "Blitzcrank": "53.66%", - "Braum": "53.22%", - "Vel'Koz": "53.08%", - "Senna": "52.78%", - "Bard": "52.75%", - "Neeko": "52.17%", - "Rakan": "51.91%", - "Soraka": "51.52%", - "Thresh": "51.33%" - } + "champion_name": "Renata Glasc", + "pick_rate": "2.4%", + "counters": "no info" }, { - "champion_name": "Alistar", - "pick_rate": "5.8%", + "champion_name": "Zyra", + "pick_rate": "4.3%", "counters": { - "Sona": "54.51%", - "Janna": "54.04%", - "Swain": "53.24%", - "Vel'Koz": "52.78%", - "Senna": "52.64%", - "Zilean": "52.39%", - "Soraka": "52.22%", - "Seraphine": "51.97%", - "Xerath": "51.86%", - "Milio": "51.85%" + "Taric": "54.73%", + "Leona": "54.05%", + "Sona": "53.31%", + "Maokai": "52.98%", + "Renata Glasc": "52.84%", + "Pyke": "52.6%", + "Zac": "52.59%", + "Janna": "52.42%", + "Zilean": "51.88%", + "Yuumi": "51.63%" } }, { - "champion_name": "Zac", - "pick_rate": "0.7%", - "counters": { - "Poppy": "67.39%", - "Maokai": "57.14%", - "Neeko": "56.72%", - "Swain": "56%", - "Vel'Koz": "54.1%", - "Taric": "53.85%", - "Alistar": "53.74%", - "Ashe": "53.49%", - "Rakan": "53.42%", - "Zilean": "53.26%" - } + "champion_name": "Vel'Koz", + "pick_rate": "1.4%", + "counters": "no info" }, { - "champion_name": "Leona", - "pick_rate": "4.8%", - "counters": { - "Taric": "56.7%", - "Janna": "54.1%", - "Bard": "54.03%", - "Shaco": "53.67%", - "Maokai": "53.55%", - "Rell": "53.22%", - "Soraka": "53.11%", - "Lux": "53.09%", - "Morgana": "53.02%", - "Braum": "51.72%" - } + "champion_name": "Senna", + "pick_rate": "11.0%", + "counters": "no info" }, { - "champion_name": "Xerath", - "pick_rate": "4.8%", - "counters": { - "Sylas": "55%", - "Maokai": "53.98%", - "Sona": "53.77%", - "Janna": "53.15%", - "Pyke": "52.84%", - "Soraka": "52.33%", - "Rakan": "52.02%", - "Leona": "51.76%", - "Zyra": "51.72%", - "Nami": "51.6%" - } + "champion_name": "Shaco", + "pick_rate": "2.6%", + "counters": "no info" }, { - "champion_name": "Nami", - "pick_rate": "6.5%", - "counters": { - "Maokai": "56.37%", - "Neeko": "53.14%", - "Blitzcrank": "53.03%", - "Rakan": "52.58%", - "Zyra": "52.29%", - "Soraka": "51.92%", - "Milio": "51.67%", - "Pyke": "51.56%", - "Janna": "51.44%", - "Bard": "51.33%" - } + "champion_name": "Pantheon", + "pick_rate": "1.8%", + "counters": "no info" }, { - "champion_name": "Shaco", + "champion_name": "Tahm Kench", "pick_rate": "1.1%", - "counters": { - "Sona": "60.95%", - "Swain": "60.22%", - "Taric": "59.68%", - "Heimerdinger": "59.02%", - "Zac": "56.14%", - "Janna": "55.96%", - "Milio": "54.49%", - "Pyke": "54.23%", - "Rakan": "53.2%", - "Bard": "52.59%" - } + "counters": "no info" }, { - "champion_name": "Heimerdinger", - "pick_rate": "1.0%", + "champion_name": "Nautilus", + "pick_rate": "11.0%", "counters": { - "Maokai": "61.64%", - "Neeko": "56.82%", - "Seraphine": "55.95%", - "Xerath": "54.86%", - "Bard": "54.55%", - "Leona": "54.4%", - "Rell": "53.15%", - "Rakan": "52.67%", - "Lulu": "52.56%", - "Janna": "52.56%" + "Taric": "60.99%", + "Leona": "57.54%", + "Braum": "56.73%", + "Renata Glasc": "55.96%", + "Alistar": "55.65%", + "Rell": "55.38%", + "Camille": "54.74%", + "Pantheon": "54.53%", + "Swain": "54.35%", + "Maokai": "53.72%" } }, { - "champion_name": "Sylas", - "pick_rate": "1.0%", + "champion_name": "Milio", + "pick_rate": "8.1%", "counters": { - "Sona": "60%", - "Janna": "57.91%", - "Vel'Koz": "57.75%", - "Braum": "57.52%", - "Morgana": "57.5%", - "Taric": "55.56%", - "Rell": "54.55%", - "Rakan": "53.89%", - "Blitzcrank": "53.74%", - "Thresh": "53.64%" + "Blitzcrank": "56.32%", + "Braum": "55.45%", + "Maokai": "54.37%", + "Camille": "53.86%", + "Janna": "53.43%", + "Thresh": "53.37%", + "Zilean": "53.04%", + "Tahm Kench": "53.04%", + "Vel'Koz": "52.98%", + "Leona": "52.96%" } }, { - "champion_name": "Tahm Kench", - "pick_rate": "0.6%", + "champion_name": "Nami", + "pick_rate": "5.5%", "counters": { - "Taric": "62.22%", - "Braum": "61.11%", - "Bard": "57.4%", - "Zyra": "57.27%", - "Pyke": "55.38%", - "Maokai": "54.9%", - "Brand": "54.67%", - "Lulu": "54.55%", - "Renata Glasc": "54.17%", - "Rakan": "54.12%" + "Morgana": "56.08%", + "Blitzcrank": "55.61%", + "Maokai": "55.57%", + "Vel'Koz": "54.47%", + "Rakan": "54.44%", + "Neeko": "54.21%", + "Rell": "53.9%", + "Nautilus": "53.32%", + "Thresh": "53.28%", + "Senna": "53.17%" } }, { - "champion_name": "Amumu", - "pick_rate": "0.6%", + "champion_name": "Xerath", + "pick_rate": "2.7%", "counters": { - "Maokai": "66.07%", - "Zyra": "61.68%", - "Rell": "58.9%", - "Rakan": "58.55%", - "Janna": "58.5%", - "Neeko": "57.14%", - "Taric": "56.1%", - "Braum": "55.41%", - "Bard": "54%", - "Leona": "53.91%" + "Blitzcrank": "58.63%", + "Neeko": "56.43%", + "Sona": "56.36%", + "Maokai": "56.26%", + "Pyke": "56.15%", + "Leona": "56.02%", + "Braum": "55.06%", + "Rakan": "54.83%", + "Nautilus": "54.65%", + "Janna": "53.86%" } }, { - "champion_name": "Twitch", - "pick_rate": "0.8%", - "counters": { - "Zilean": "61.97%", - "Taric": "60.47%", - "Leona": "57.92%", - "Soraka": "57.87%", - "Shaco": "57.38%", - "Nami": "57.14%", - "Rakan": "56.34%", - "Sylas": "55.56%", - "Brand": "55.26%", - "Pyke": "54.65%" - } + "champion_name": "Brand", + "pick_rate": "1.9%", + "counters": "no info" }, { - "champion_name": "Nautilus", - "pick_rate": "10.9%", + "champion_name": "Swain", + "pick_rate": "1.4%", "counters": { - "Taric": "55.98%", - "Rell": "55.66%", - "Renata Glasc": "55.13%", - "Sylas": "55.09%", - "Braum": "54.08%", - "Rakan": "54.06%", - "Vel'Koz": "53.45%", - "Swain": "53.09%", - "Neeko": "52.97%", - "Alistar": "52.87%" + "Zac": "58.97%", + "Sona": "58.67%", + "Neeko": "57.41%", + "Nami": "57.14%", + "Milio": "56.44%", + "Pantheon": "55.95%", + "Zyra": "55.49%", + "Morgana": "55.24%", + "Leona": "55%", + "Camille": "54.79%" } }, { - "champion_name": "Ashe", - "pick_rate": "5.7%", + "champion_name": "Lulu", + "pick_rate": "7.9%", "counters": { - "Pyke": "56.98%", - "Sylas": "54.95%", - "Blitzcrank": "54.83%", - "Heimerdinger": "54.8%", - "Rell": "54.67%", - "Nautilus": "54.35%", - "Bard": "53.44%", - "Milio": "53.1%", - "Rakan": "52.69%", - "Brand": "52.5%" + "Vel'Koz": "57.35%", + "Blitzcrank": "56.23%", + "Zilean": "55.78%", + "Tahm Kench": "55.43%", + "Renata Glasc": "54.86%", + "Rell": "54.7%", + "Janna": "54.6%", + "Maokai": "54.51%", + "Braum": "53.79%", + "Ashe": "53.65%" } }, { - "champion_name": "Lux", - "pick_rate": "5.1%", - "counters": { - "Blitzcrank": "56.57%", - "Maokai": "54.94%", - "Zyra": "54.66%", - "Shaco": "54.45%", - "Vel'Koz": "54.1%", - "Sylas": "53.87%", - "Zilean": "53.51%", - "Pyke": "53.5%", - "Xerath": "52.75%", - "Nami": "52.72%" - } + "champion_name": "Morgana", + "pick_rate": "2.8%", + "counters": "no info" }, { - "champion_name": "Brand", - "pick_rate": "2.0%", - "counters": { - "Sona": "58.11%", - "Maokai": "57.24%", - "Xerath": "57.03%", - "Janna": "56.31%", - "Pyke": "56.01%", - "Zyra": "55.13%", - "Leona": "54.96%", - "Zilean": "54.92%", - "Soraka": "54.73%", - "Senna": "54.19%" - } + "champion_name": "Heimerdinger", + "pick_rate": "0.5%", + "counters": "no info" }, { - "champion_name": "Seraphine", - "pick_rate": "2.0%", + "champion_name": "Ashe", + "pick_rate": "4.8%", "counters": { - "Sylas": "58.33%", - "Maokai": "57.24%", - "Zilean": "56.09%", - "Bard": "55.97%", - "Pyke": "54.69%", - "Soraka": "53.72%", - "Blitzcrank": "53.61%", - "Yuumi": "53.6%", - "Milio": "52.99%", - "Thresh": "52.66%" + "Camille": "58.72%", + "Maokai": "57.34%", + "Pyke": "56.14%", + "Blitzcrank": "55.88%", + "Nautilus": "55.46%", + "Taric": "55.41%", + "Vel'Koz": "54.55%", + "Zyra": "54.13%", + "Soraka": "53.87%", + "Xerath": "53.83%" } }, { - "champion_name": "Lulu", - "pick_rate": "7.4%", + "champion_name": "Sylas", + "pick_rate": "0.6%", "counters": { - "Taric": "56.84%", - "Zilean": "54.43%", - "Neeko": "54.04%", - "Senna": "54.04%", - "Sona": "53.78%", - "Zyra": "53.7%", - "Maokai": "53.46%", - "Blitzcrank": "52.96%", - "Bard": "52.89%", - "Thresh": "52.88%" + "Taric": "70%", + "Shaco": "70%", + "Pantheon": "64.86%", + "Neeko": "63.16%", + "Thresh": "58.58%", + "Lulu": "58.52%", + "Morgana": "58.06%", + "Janna": "57.66%", + "Leona": "57.58%", + "Bard": "57.45%" } }, { - "champion_name": "Morgana", - "pick_rate": "3.2%", + "champion_name": "Neeko", + "pick_rate": "1.6%", "counters": { - "Zyra": "56.73%", - "Senna": "56.35%", - "Rell": "56.29%", - "Janna": "55.6%", - "Milio": "55.2%", - "Vel'Koz": "55.08%", - "Nami": "54.67%", - "Taric": "54.31%", - "Karma": "54.24%", - "Maokai": "53.2%" + "Zilean": "60.66%", + "Taric": "58.7%", + "Zac": "57.78%", + "Leona": "57.76%", + "Pantheon": "57.14%", + "Janna": "56.74%", + "Blitzcrank": "56.38%", + "Bard": "55.85%", + "Camille": "55.41%", + "Rell": "55.19%" } }, { "champion_name": "Karma", - "pick_rate": "7.6%", - "counters": { - "Pyke": "57.3%", - "Maokai": "55.33%", - "Zyra": "54.97%", - "Blitzcrank": "54.92%", - "Rakan": "54.52%", - "Janna": "54.36%", - "Sona": "54.33%", - "Nautilus": "54.21%", - "Soraka": "53.61%", - "Neeko": "52.83%" - } + "pick_rate": "9.6%", + "counters": "no info" }, { - "champion_name": "Swain", - "pick_rate": "1.6%", + "champion_name": "Lux", + "pick_rate": "3.9%", "counters": { - "Zilean": "57.77%", - "Sona": "57.32%", - "Vel'Koz": "57.25%", - "Janna": "56.41%", - "Brand": "55.87%", - "Yuumi": "55.78%", - "Zyra": "55.78%", - "Xerath": "55.48%", - "Soraka": "55.34%", - "Senna": "55.32%" + "Camille": "62.93%", + "Pantheon": "60%", + "Pyke": "58.92%", + "Bard": "57.24%", + "Shaco": "56.3%", + "Blitzcrank": "55.42%", + "Maokai": "55.39%", + "Leona": "55.08%", + "Alistar": "54.69%", + "Zilean": "54.61%" } }, { - "champion_name": "Pantheon", - "pick_rate": "1.0%", - "counters": { - "Neeko": "59.74%", - "Maokai": "58.43%", - "Karma": "57.22%", - "Bard": "57.14%", - "Swain": "56.25%", - "Rakan": "55.92%", - "Brand": "55.83%", - "Soraka": "55.74%", - "Sona": "55.56%", - "Janna": "55.42%" - } + "champion_name": "Seraphine", + "pick_rate": "1.7%", + "counters": "no info" }, { "champion_name": "Yuumi", - "pick_rate": "4.9%", + "pick_rate": "4.4%", "counters": { - "Maokai": "61.28%", - "Rell": "59.85%", - "Taric": "57.47%", - "Alistar": "57.31%", - "Nautilus": "56.64%", - "Rakan": "55.78%", - "Pyke": "55.71%", - "Leona": "55.63%", - "Sylas": "55.27%", - "Thresh": "55.23%" + "Rell": "62.65%", + "Braum": "58.82%", + "Blitzcrank": "58.05%", + "Leona": "57.77%", + "Pyke": "57.61%", + "Taric": "57.46%", + "Swain": "56.85%", + "Rakan": "56.72%", + "Maokai": "56.12%", + "Janna": "55.85%" } }, + { + "champion_name": "Twitch", + "pick_rate": "0.5%", + "counters": "no info" + }, { "champion_name": "Hwei", - "pick_rate": "2.1%", - "counters": { - "Taric": "66.91%", - "Sona": "66.55%", - "Blitzcrank": "65.66%", - "Janna": "65.3%", - "Brand": "65.18%", - "Pyke": "63.85%", - "Xerath": "63.02%", - "Vel'Koz": "62.71%", - "Morgana": "62.31%", - "Milio": "62.01%" + "pick_rate": "2.9%", + "counters": { + "Camille": "63.76%", + "Pyke": "60.47%", + "Maokai": "58.72%", + "Xerath": "58.7%", + "Rakan": "58.47%", + "Taric": "58.43%", + "Swain": "58.1%", + "Zilean": "57.69%", + "Vel'Koz": "57.69%", + "Janna": "57.32%" } } ] \ No newline at end of file diff --git a/src/LoL-support/support_names.json b/src/LoL-support/support_names.json old mode 100644 new mode 100755 index bafadeb..6655ed7 --- a/src/LoL-support/support_names.json +++ b/src/LoL-support/support_names.json @@ -1 +1 @@ -["Taric", "Janna", "Maokai", "Bard", "Rakan", "Sona", "Senna", "Rell", "Neeko", "Soraka", "Blitzcrank", "Zyra", "Pyke", "Zilean", "Braum", "Renata", "Vel'Koz", "Thresh", "Milio", "Alistar", "Zac", "Leona", "Xerath", "Nami", "Shaco", "Heimerdinger", "Sylas", "Tahm Kench", "Amumu", "Twitch", "Nautilus", "Ashe", "Lux", "Brand", "Seraphine", "Lulu", "Morgana", "Karma", "Swain", "Pantheon", "Yuumi", "Hwei"] \ No newline at end of file +["Maokai", "Janna", "Taric", "Braum", "Blitzcrank", "Camille", "Leona", "Rell", "Rakan", "Zilean", "Pyke", "Zac", "Alistar", "Bard", "Soraka", "Sona", "Thresh", "Renata Glasc", "Zyra", "Vel'Koz", "Senna", "Shaco", "Pantheon", "Tahm Kench", "Nautilus", "Milio", "Nami", "Xerath", "Brand", "Swain", "Lulu", "Morgana", "Heimerdinger", "Ashe", "Sylas", "Neeko", "Karma", "Lux", "Seraphine", "Yuumi", "Twitch", "Hwei"] \ No newline at end of file diff --git a/src/LoL-support/synergies_list.json b/src/LoL-support/synergies_list.json old mode 100644 new mode 100755 index 391b717..3d7539f --- a/src/LoL-support/synergies_list.json +++ b/src/LoL-support/synergies_list.json @@ -1,692 +1,559 @@ [ { - "champion_name": "Taric", + "champion_name": "Janna", "synergy": { - "Karthus": "70.83%", - "Yasuo": "68.91%", - "Lee Sin": "68.18%", - "Master Yi": "59.26%", - "Kog'Maw": "58.82%", - "Sivir": "58.62%", - "Varus": "56.63%", - "Swain": "56.45%", - "Seraphine": "56.41%", - "Kalista": "55.94%", - "Ziggs": "55.00%", - "Vayne": "54.01%", - "Jinx": "52.38%", - "Lucian": "51.91%", - "Kai'Sa": "51.81%", - "Ashe": "51.67%", - "Miss Fortune": "50.94%", - "Ezreal": "50.20%", - "Twitch": "50.00%" + "Nilah": "57.38%", + "Twitch": "57.02%", + "Senna": "56.68%", + "Seraphine": "55.59%", + "Miss Fortune": "54.71%", + "Zeri": "54.30%", + "Tristana": "54.22%", + "Draven": "54.22%", + "Twisted Fate": "53.57%", + "Samira": "53.57%", + "Jhin": "53.03%", + "Lucian": "52.84%", + "Karthus": "52.73%", + "Ezreal": "52.60%", + "Yasuo": "52.58%", + "Kalista": "52.35%", + "Vayne": "51.97%", + "Ashe": "51.80%", + "Sivir": "51.10%", + "Caitlyn": "51.05%", + "Smolder": "50.23%", + "Aphelios": "50.23%" } }, { - "champion_name": "Janna", + "champion_name": "Taric", "synergy": { - "Seraphine": "58.66%", - "Vayne": "55.43%", - "Yasuo": "54.91%", - "Ziggs": "54.85%", - "Jinx": "53.96%", - "Ashe": "53.85%", - "Tristana": "53.63%", - "Kalista": "53.47%", - "Kog'Maw": "52.89%", - "Karthus": "52.83%", - "Jhin": "52.65%", - "Ezreal": "52.58%", - "Lucian": "52.51%", - "Draven": "52.25%", - "Twitch": "52.00%", - "Samira": "51.88%", - "Varus": "51.73%", - "Zeri": "51.64%", - "Caitlyn": "51.53%", - "Kai'Sa": "50.95%" + "Vayne": "60.16%", + "Ashe": "58.46%", + "Yasuo": "58.33%", + "Twisted Fate": "57.89%", + "Pyke": "57.14%", + "Senna": "56.59%", + "Nilah": "56.32%", + "Kalista": "55.13%", + "Jinx": "54.39%", + "Aphelios": "53.57%", + "Seraphine": "53.13%", + "Zeri": "52.54%", + "Twitch": "52.05%", + "Smolder": "51.89%", + "Ezreal": "51.67%", + "Kai'Sa": "50.59%", + "Sivir": "50.00%", + "Lucian": "50.00%", + "Dr. Mundo": "50.00%", + "Xayah": "50.00%", + "Karthus": "50.00%" } }, { - "champion_name": "Maokai", + "champion_name": "Blitzcrank", "synergy": { - "Xayah": "60.00%", - "Ziggs": "58.33%", - "Karthus": "57.89%", - "Kai'Sa": "53.95%", - "Vayne": "53.51%", - "Sivir": "52.63%", - "Jhin": "52.46%", - "Aphelios": "52.46%", - "Draven": "52.38%", - "Ezreal": "52.29%", - "Samira": "51.81%", - "Varus": "51.53%", - "Zeri": "51.02%", + "Seraphine": "65.22%", + "Karthus": "57.78%", + "Draven": "56.57%", + "Vayne": "55.39%", + "Varus": "54.74%", + "Jinx": "54.30%", + "Twisted Fate": "54.17%", + "Tristana": "53.18%", + "Sivir": "53.06%", + "Senna": "52.85%", + "Aphelios": "52.14%", + "Kai'Sa": "51.65%", + "Lucian": "50.78%", + "Nilah": "50.67%", "Twitch": "50.00%", "Ashe": "50.00%", - "Hwei": "50.00%" + "Zeri": "50.00%" } }, { - "champion_name": "Bard", + "champion_name": "Camille", "synergy": { - "Nilah": "59.77%", - "Vayne": "55.15%", - "Caitlyn": "55.12%", - "Twitch": "55.06%", - "Ziggs": "54.37%", - "Tristana": "53.92%", - "Karthus": "53.62%", - "Seraphine": "53.05%", - "Lucian": "52.97%", - "Ezreal": "52.51%", - "Ashe": "52.44%", - "Jhin": "51.89%", - "Swain": "51.69%", - "Jinx": "51.34%", - "Zeri": "50.76%" - } - }, - { - "champion_name": "Rakan", - "synergy": { - "Lucian": "56.02%", - "Jinx": "54.48%", - "Twitch": "54.35%", - "Seraphine": "54.17%", - "Ziggs": "54.08%", - "Vayne": "54.03%", - "Sivir": "53.73%", - "Jhin": "52.40%", - "Yasuo": "51.97%", - "Karthus": "51.90%", - "Ezreal": "51.77%", - "Ashe": "51.55%", - "Miss Fortune": "50.65%", - "Nilah": "50.61%", - "Varus": "50.35%", - "Draven": "50.12%" + "Twitch": "62.67%", + "Sivir": "58.33%", + "Zeri": "56.47%", + "Tristana": "56.20%", + "Senna": "55.81%", + "Xayah": "55.26%", + "Aphelios": "53.62%", + "Nilah": "53.13%", + "Seraphine": "52.94%", + "Varus": "52.89%", + "Karthus": "52.63%", + "Ashe": "52.50%", + "Smolder": "52.34%", + "Jhin": "52.09%", + "Draven": "52.06%", + "Ziggs": "52.00%", + "Twisted Fate": "50.94%", + "Vayne": "50.92%", + "Lucian": "50.35%", + "Swain": "50.00%" } }, { - "champion_name": "Sona", + "champion_name": "Leona", "synergy": { - "Yasuo": "64.52%", - "Tristana": "60.29%", - "Ziggs": "58.51%", - "Samira": "57.45%", - "Sivir": "56.60%", - "Vayne": "56.55%", - "Nilah": "55.88%", - "Kog'Maw": "53.49%", - "Lucian": "53.04%", - "Xayah": "52.90%", - "Seraphine": "52.76%", - "Ezreal": "52.71%", - "Miss Fortune": "52.23%", - "Varus": "51.05%", - "Ashe": "50.93%", - "Caitlyn": "50.81%", - "Twitch": "50.21%" - } - }, - { - "champion_name": "Senna", - "synergy": { - "Kalista": "57.48%", - "Nilah": "56.48%", - "Karthus": "56.18%", - "Zeri": "55.11%", - "Seraphine": "55.06%", - "Miss Fortune": "54.98%", - "Cho'Gath": "54.69%", - "Vayne": "54.60%", - "Ashe": "54.54%", - "Tahm Kench": "54.51%", - "Yasuo": "54.49%", - "Brand": "54.26%", - "Twitch": "54.15%", - "Swain": "53.87%", - "Jhin": "52.87%", - "Ziggs": "52.59%", - "Sivir": "52.21%", - "Ezreal": "51.36%", - "Tristana": "51.36%", - "Varus": "50.75%" + "Swain": "68.09%", + "Senna": "60.41%", + "Jinx": "59.02%", + "Seraphine": "58.90%", + "Ashe": "57.67%", + "Aphelios": "56.67%", + "Nilah": "56.52%", + "Vayne": "55.20%", + "Lucian": "54.63%", + "Smolder": "54.42%", + "Miss Fortune": "53.39%", + "Karthus": "53.06%", + "Xayah": "51.61%", + "Ezreal": "51.04%", + "Kalista": "50.59%", + "Twisted Fate": "50.31%" } }, { "champion_name": "Rell", "synergy": { - "Ziggs": "61.31%", - "Karthus": "60.18%", - "Seraphine": "59.02%", - "Tristana": "58.16%", - "Lucian": "57.30%", - "Twitch": "56.30%", - "Miss Fortune": "54.44%", - "Ashe": "53.88%", - "Sivir": "53.49%", - "Jinx": "52.83%", - "Draven": "52.25%", - "Xayah": "52.10%", - "Ezreal": "52.08%", - "Kai'Sa": "51.33%", - "Samira": "51.15%", - "Zeri": "50.35%", - "Yasuo": "50.25%", - "Aphelios": "50.21%" - } - }, - { - "champion_name": "Neeko", - "synergy": { - "Nilah": "77.78%", - "Yasuo": "59.46%", - "Xayah": "59.26%", - "Hwei": "58.82%", - "Lucian": "56.79%", - "Vayne": "53.85%", - "Twitch": "53.17%", - "Kalista": "50.75%", - "Ezreal": "50.72%", - "Ashe": "50.29%", - "Quinn": "50.00%", - "Samira": "50.00%", - "Varus": "50.00%" - } - }, - { - "champion_name": "Soraka", - "synergy": { - "Yasuo": "63.54%", - "Swain": "59.70%", - "Samira": "58.41%", - "Vayne": "57.03%", - "Ashe": "53.73%", - "Jinx": "53.47%", - "Ziggs": "53.38%", - "Jhin": "53.17%", - "Tristana": "52.80%", - "Lucian": "52.34%", - "Miss Fortune": "51.96%", - "Twitch": "51.63%", - "Draven": "50.30%" - } - }, - { - "champion_name": "Blitzcrank", - "synergy": { - "Nilah": "58.59%", - "Xayah": "55.50%", - "Draven": "54.17%", - "Lucian": "53.85%", - "Ashe": "53.42%", - "Vayne": "52.91%", - "Twitch": "52.80%", - "Seraphine": "52.34%", - "Jinx": "51.90%", - "Karthus": "51.89%", - "Varus": "51.87%", - "Tristana": "51.72%", - "Miss Fortune": "51.53%", - "Zeri": "51.27%", - "Aphelios": "50.95%", - "Kalista": "50.84%", - "Jhin": "50.74%", - "Ezreal": "50.08%", - "Caitlyn": "50.00%" - } - }, - { - "champion_name": "Zyra", - "synergy": { - "Seraphine": "55.06%", - "Vayne": "54.84%", - "Tristana": "54.05%", - "Twitch": "52.96%", - "Samira": "51.98%", - "Jhin": "51.51%", - "Karthus": "51.16%", - "Draven": "50.65%", - "Xayah": "50.00%" - } - }, - { - "champion_name": "Pyke", - "synergy": { - "Karthus": "61.98%", - "Nilah": "60.43%", - "Yasuo": "57.30%", - "Tristana": "55.96%", - "Twitch": "55.08%", - "Ashe": "54.27%", - "Swain": "53.91%", - "Ziggs": "53.28%", - "Seraphine": "52.82%", - "Vayne": "52.64%", - "Varus": "51.58%", - "Jhin": "51.51%", - "Ezreal": "51.49%", - "Hwei": "51.43%", - "Draven": "50.95%" + "Xayah": "58.33%", + "Twisted Fate": "56.72%", + "Twitch": "56.48%", + "Nilah": "56.25%", + "Miss Fortune": "55.88%", + "Zeri": "55.45%", + "Senna": "55.08%", + "Vayne": "53.65%", + "Jinx": "53.40%", + "Kalista": "52.46%", + "Samira": "52.46%", + "Aphelios": "52.17%", + "Draven": "52.15%", + "Tristana": "51.90%", + "Karthus": "51.43%", + "Ezreal": "51.32%", + "Jhin": "50.43%", + "Yasuo": "50.00%" } }, { "champion_name": "Zilean", "synergy": { - "Swain": "58.97%", - "Hwei": "55.56%", - "Samira": "54.12%", - "Kog'Maw": "54.05%", - "Twitch": "53.80%", - "Aphelios": "53.47%", - "Vayne": "51.88%", - "Caitlyn": "51.79%", - "Seraphine": "51.67%", - "Ezreal": "51.07%", - "Ashe": "50.12%" - } - }, - { - "champion_name": "Braum", - "synergy": { - "Sivir": "60.82%", - "Twitch": "59.55%", - "Xayah": "54.40%", - "Varus": "52.69%", - "Kog'Maw": "52.03%", - "Ashe": "51.92%", - "Jinx": "51.32%", - "Tristana": "50.94%", - "Vayne": "50.83%", - "Draven": "50.75%", - "Ezreal": "50.35%" - } - }, - { - "champion_name": "Vel'Koz", - "synergy": { - "Karthus": "61.90%", - "Vayne": "55.34%", - "Sivir": "54.72%", - "Ashe": "54.44%", - "Aphelios": "53.25%", - "Seraphine": "52.38%", - "Jhin": "51.92%", - "Caitlyn": "51.43%", - "Ezreal": "51.22%", - "Twitch": "50.39%", - "Varus": "50.37%", - "Tristana": "50.00%", - "Miss Fortune": "50.00%" + "Miss Fortune": "62.03%", + "Twisted Fate": "61.05%", + "Draven": "54.63%", + "Swain": "54.55%", + "Vayne": "54.47%", + "Ashe": "53.97%", + "Caitlyn": "53.13%", + "Seraphine": "52.44%", + "Smolder": "51.37%", + "Xayah": "51.22%", + "Sivir": "50.00%" } }, { - "champion_name": "Thresh", + "champion_name": "Pyke", "synergy": { - "Tristana": "55.29%", - "Hwei": "54.01%", - "Draven": "53.28%", - "Jinx": "52.84%", - "Aphelios": "51.39%", - "Twitch": "51.28%", - "Kalista": "51.17%", - "Samira": "51.08%", - "Vayne": "51.01%", - "Karthus": "50.91%", - "Lucian": "50.61%", - "Ashe": "50.26%", - "Yasuo": "50.24%" + "Karthus": "60.47%", + "Swain": "58.06%", + "Senna": "57.89%", + "Ashe": "55.02%", + "Twitch": "54.55%", + "Vayne": "53.90%", + "Jinx": "53.67%", + "Yasuo": "53.66%", + "Seraphine": "53.19%", + "Twisted Fate": "53.07%", + "Nilah": "53.00%", + "Smolder": "51.63%", + "Zeri": "50.56%", + "Ezreal": "50.22%", + "Draven": "50.00%" } }, { - "champion_name": "Milio", + "champion_name": "Zac", "synergy": { - "Kog'Maw": "53.82%", - "Xayah": "52.76%", - "Lucian": "51.94%", - "Zeri": "51.56%", - "Vayne": "51.56%", - "Kalista": "51.56%", - "Ashe": "51.48%", - "Draven": "50.69%", - "Aphelios": "50.42%", - "Twitch": "50.00%" + "Aphelios": "83.33%", + "Caitlyn": "66.67%", + "Draven": "65.52%", + "Senna": "64.71%", + "Tristana": "61.54%", + "Twisted Fate": "61.29%", + "Zeri": "58.82%", + "Seraphine": "56.25%", + "Jinx": "55.00%", + "Ezreal": "52.38%", + "Smolder": "51.58%", + "Kalista": "51.52%", + "Xayah": "50.00%" } }, { "champion_name": "Alistar", "synergy": { - "Seraphine": "59.43%", - "Ziggs": "57.86%", - "Sivir": "55.20%", - "Hwei": "54.65%", - "Zeri": "53.37%", - "Ashe": "52.55%", - "Yasuo": "52.42%", - "Vayne": "52.01%", - "Twitch": "51.83%", - "Xayah": "51.49%", - "Jinx": "50.82%", - "Jhin": "50.68%", - "Ezreal": "50.58%", - "Draven": "50.28%", - "Miss Fortune": "50.20%" + "Karthus": "64.86%", + "Seraphine": "62.86%", + "Zeri": "60.83%", + "Sivir": "59.26%", + "Jinx": "58.42%", + "Yasuo": "58.09%", + "Tristana": "55.84%", + "Senna": "53.27%", + "Twitch": "52.99%", + "Draven": "51.74%", + "Xayah": "51.25%", + "Ezreal": "50.91%", + "Twisted Fate": "50.75%", + "Samira": "50.00%" } }, { - "champion_name": "Zac", + "champion_name": "Bard", "synergy": { - "Ziggs": "72.73%", - "Sivir": "71.43%", - "Zeri": "70.00%", - "Varus": "63.64%", - "Yasuo": "62.07%", - "Jhin": "59.32%", - "Kalista": "57.14%", - "Hwei": "57.14%", - "Twitch": "55.32%", - "Ezreal": "52.63%", - "Miss Fortune": "52.38%", - "Vayne": "50.75%", - "Swain": "50.00%", - "Tristana": "50.00%", - "Caitlyn": "50.00%" + "Yasuo": "60.34%", + "Seraphine": "59.53%", + "Senna": "59.42%", + "Ziggs": "57.58%", + "Caitlyn": "55.43%", + "Xayah": "54.26%", + "Hwei": "54.17%", + "Twisted Fate": "53.66%", + "Jinx": "52.43%", + "Twitch": "51.68%", + "Aphelios": "51.54%", + "Karthus": "51.47%", + "Samira": "50.60%", + "Vayne": "50.34%" } }, { - "champion_name": "Leona", + "champion_name": "Sona", "synergy": { - "Seraphine": "56.47%", - "Xayah": "55.00%", - "Zeri": "54.63%", - "Ezreal": "54.35%", - "Twitch": "54.27%", - "Caitlyn": "53.68%", - "Jhin": "53.21%", - "Yasuo": "53.19%", - "Lucian": "52.72%", - "Miss Fortune": "51.92%", - "Aphelios": "51.70%", - "Tristana": "51.48%", - "Kalista": "51.16%" + "Nilah": "71.43%", + "Tristana": "63.16%", + "Twisted Fate": "56.19%", + "Jhin": "55.34%", + "Kai'Sa": "55.11%", + "Varus": "52.63%", + "Aphelios": "52.00%", + "Twitch": "51.46%", + "Vayne": "51.22%", + "Smolder": "50.72%", + "Lucian": "50.00%", + "Samira": "50.00%" } }, { - "champion_name": "Xerath", + "champion_name": "Thresh", "synergy": { - "Ziggs": "55.24%", - "Jinx": "53.95%", - "Vayne": "53.32%", - "Samira": "52.63%", - "Xayah": "52.57%", - "Ashe": "52.33%", - "Jhin": "51.81%", - "Caitlyn": "51.12%", - "Hwei": "50.88%", - "Tristana": "50.70%", - "Ezreal": "50.00%" + "Yasuo": "62.82%", + "Senna": "58.33%", + "Xayah": "56.52%", + "Sivir": "56.25%", + "Seraphine": "55.74%", + "Kalista": "54.41%", + "Twitch": "52.68%", + "Miss Fortune": "52.44%", + "Jinx": "52.43%", + "Draven": "52.37%", + "Aphelios": "52.12%", + "Smolder": "51.38%", + "Tristana": "51.11%", + "Samira": "50.59%", + "Varus": "50.32%", + "Vayne": "50.15%", + "Kai'Sa": "50.05%" } }, { - "champion_name": "Nami", + "champion_name": "Zyra", "synergy": { - "Swain": "59.52%", - "Brand": "55.69%", - "Karthus": "55.65%", - "Twitch": "52.16%", - "Ashe": "51.88%", - "Seraphine": "51.43%", - "Jhin": "51.32%", - "Vayne": "51.29%", - "Ezreal": "51.27%" + "Xayah": "66.07%", + "Sivir": "61.54%", + "Twisted Fate": "53.68%", + "Miss Fortune": "53.25%", + "Ezreal": "52.52%", + "Senna": "52.28%", + "Tristana": "50.94%", + "Vayne": "50.18%", + "Jinx": "50.00%" } }, { - "champion_name": "Shaco", + "champion_name": "Vel'Koz", "synergy": { - "Seraphine": "85.71%", - "Miss Fortune": "58.33%", - "Samira": "54.84%", - "Caitlyn": "54.43%", - "Jhin": "54.01%", - "Varus": "52.43%", + "Samira": "65.00%", + "Nilah": "60.00%", + "Hwei": "58.82%", + "Seraphine": "54.55%", + "Draven": "53.66%", + "Ashe": "53.52%", + "Tristana": "53.33%", + "Zeri": "53.13%", + "Senna": "51.90%", + "Kai'Sa": "51.02%", + "Jhin": "50.71%", + "Smolder": "50.22%", + "Sivir": "50.00%", "Karthus": "50.00%" } }, { - "champion_name": "Heimerdinger", + "champion_name": "Shaco", "synergy": { - "Zeri": "68.00%", - "Sivir": "57.89%", - "Draven": "54.29%", - "Jhin": "54.03%", - "Vayne": "53.49%", - "Miss Fortune": "52.78%", - "Ashe": "50.34%", - "Kalista": "50.00%", - "Twitch": "50.00%" + "Karthus": "76.92%", + "Twisted Fate": "62.64%", + "Tristana": "58.62%", + "Senna": "56.19%", + "Miss Fortune": "56.10%", + "Aphelios": "52.78%", + "Hwei": "52.63%", + "Ashe": "52.48%", + "Draven": "52.24%", + "Nilah": "50.00%", + "Caitlyn": "50.00%", + "Ziggs": "50.00%" } }, { - "champion_name": "Sylas", + "champion_name": "Pantheon", "synergy": { - "Karthus": "60.00%", - "Aphelios": "58.06%", - "Nilah": "57.89%", - "Miss Fortune": "57.14%", - "Kalista": "55.36%", - "Lucian": "54.65%", - "Seraphine": "54.55%", - "Sivir": "52.94%", - "Zeri": "52.63%", - "Vayne": "52.53%", - "Jhin": "52.38%", - "Ezreal": "51.14%", - "Twitch": "50.77%", - "Yasuo": "50.00%" + "Karthus": "66.67%", + "Twisted Fate": "58.82%", + "Miss Fortune": "56.60%", + "Senna": "56.59%", + "Jhin": "55.67%", + "Nilah": "54.55%", + "Ezreal": "54.37%", + "Swain": "53.85%", + "Lucian": "53.06%", + "Ashe": "52.38%", + "Smolder": "52.07%", + "Samira": "51.97%", + "Jinx": "51.72%" } }, { "champion_name": "Tahm Kench", "synergy": { - "Veigar": "80.00%", - "Ziggs": "75.00%", - "Karthus": "66.67%", - "Seraphine": "58.33%", - "Xayah": "58.33%", - "Jhin": "55.56%", - "Miss Fortune": "51.72%", - "Twitch": "51.28%", - "Kog'Maw": "50.00%", - "Kalista": "50.00%", - "Ashe": "50.00%", - "Ezreal": "50.00%" + "Kalista": "68.75%", + "Jinx": "65.00%", + "Nilah": "62.50%", + "Xayah": "62.50%", + "Twitch": "57.14%", + "Varus": "55.10%", + "Twisted Fate": "54.55%", + "Zeri": "53.85%", + "Senna": "52.51%", + "Seraphine": "50.00%" } }, { - "champion_name": "Amumu", - "synergy": { - "Yasuo": "71.43%", - "Ziggs": "66.67%", - "Kalista": "64.15%", - "Lucian": "60.00%", - "Karthus": "60.00%", - "Hwei": "57.14%", - "Ezreal": "55.88%", - "Nilah": "55.56%", - "Draven": "55.41%", - "Miss Fortune": "54.37%", - "Ashe": "54.17%", - "Jinx": "53.85%", - "Kai'Sa": "53.60%", - "Tristana": "52.63%", - "Zeri": "50.00%", - "Veigar": "50.00%" + "champion_name": "Nautilus", + "synergy": { + "Seraphine": "55.21%", + "Senna": "54.59%", + "Kalista": "52.55%", + "Xayah": "52.25%", + "Caitlyn": "51.40%", + "Karthus": "51.11%", + "Twitch": "50.96%", + "Lucian": "50.86%", + "Jhin": "50.64%", + "Draven": "50.62%", + "Vayne": "50.27%", + "Zeri": "50.21%", + "Samira": "50.09%", + "Nilah": "50.00%" } }, { - "champion_name": "Twitch", + "champion_name": "Xerath", "synergy": { - "Sivir": "70.97%", - "Swain": "70.00%", - "Seraphine": "63.64%", - "Draven": "57.89%", - "Cassiopeia": "53.85%", - "Vayne": "53.54%", - "Jhin": "53.40%", - "Jinx": "52.27%", - "Ziggs": "51.72%", - "Zeri": "51.61%", - "Nilah": "50.00%", - "Kalista": "50.00%", - "Hwei": "50.00%", - "Tristana": "50.00%" + "Samira": "70.00%", + "Twisted Fate": "61.36%", + "Caitlyn": "57.97%", + "Karthus": "55.56%", + "Ashe": "52.46%", + "Vayne": "50.55%", + "Zeri": "50.00%" } }, { - "champion_name": "Nautilus", + "champion_name": "Brand", "synergy": { - "Seraphine": "58.68%", - "Ziggs": "55.43%", - "Karthus": "55.21%", - "Lucian": "51.71%", - "Tristana": "51.50%", - "Samira": "51.14%", - "Kalista": "50.71%", - "Jinx": "50.13%" + "Samira": "59.09%", + "Yasuo": "57.89%", + "Twisted Fate": "57.14%", + "Xayah": "56.52%", + "Jinx": "55.88%", + "Jhin": "53.98%", + "Draven": "53.70%", + "Seraphine": "52.50%", + "Aphelios": "50.00%" } }, { - "champion_name": "Ashe", + "champion_name": "Morgana", "synergy": { - "Karthus": "56.83%", - "Seraphine": "54.60%", - "Jinx": "54.17%", - "Nilah": "53.85%", - "Twitch": "53.55%", - "Ezreal": "51.69%", - "Swain": "51.32%", - "Ziggs": "50.79%", - "Lucian": "50.00%", - "Zeri": "50.00%" + "Xayah": "60.00%", + "Draven": "57.81%", + "Nilah": "54.55%", + "Samira": "51.61%", + "Sivir": "50.00%", + "Seraphine": "50.00%" } }, { - "champion_name": "Lux", + "champion_name": "Heimerdinger", "synergy": { - "Karthus": "59.46%", - "Twitch": "53.57%", - "Xayah": "52.17%", - "Ashe": "51.05%", - "Varus": "50.88%", - "Tristana": "50.67%", + "Tristana": "80.00%", + "Aphelios": "80.00%", + "Draven": "64.29%", + "Twitch": "62.50%", + "Twisted Fate": "60.00%", + "Jhin": "55.56%", + "Varus": "53.85%", + "Senna": "51.28%", + "Miss Fortune": "50.00%", "Jinx": "50.00%", - "Zeri": "50.00%" + "Nilah": "50.00%" } }, { - "champion_name": "Brand", + "champion_name": "Ashe", "synergy": { - "Seraphine": "57.69%", - "Karthus": "55.17%", - "Yasuo": "55.17%", - "Kalista": "53.49%", - "Tristana": "52.31%", - "Xayah": "51.40%", - "Varus": "50.17%" + "Jinx": "53.73%", + "Jhin": "53.36%", + "Karthus": "52.31%", + "Kalista": "51.90%", + "Twisted Fate": "51.81%", + "Zeri": "50.65%" } }, { - "champion_name": "Seraphine", + "champion_name": "Sylas", "synergy": { - "Vayne": "59.62%", - "Tristana": "59.38%", - "Miss Fortune": "52.50%", - "Ashe": "52.20%", - "Jinx": "51.11%", - "Draven": "50.94%" + "Miss Fortune": "75.00%", + "Zeri": "72.73%", + "Twitch": "68.75%", + "Kalista": "68.42%", + "Draven": "65.22%", + "Tristana": "61.54%", + "Senna": "60.00%", + "Aphelios": "60.00%", + "Jinx": "55.56%", + "Jhin": "50.00%" } }, { - "champion_name": "Lulu", + "champion_name": "Neeko", "synergy": { - "Miss Fortune": "55.96%", - "Sivir": "55.62%", - "Tristana": "55.22%", - "Ashe": "51.59%", + "Yasuo": "67.74%", + "Tristana": "66.67%", + "Twitch": "65.63%", + "Ashe": "60.87%", + "Senna": "59.22%", + "Nilah": "57.14%", + "Caitlyn": "54.55%", "Draven": "51.52%", - "Varus": "51.22%", - "Twitch": "51.14%", - "Jinx": "50.76%", - "Kog'Maw": "50.00%", - "Kalista": "50.00%" + "Twisted Fate": "50.94%", + "Vayne": "50.00%", + "Jinx": "50.00%", + "Miss Fortune": "50.00%" } }, { - "champion_name": "Morgana", + "champion_name": "Karma", "synergy": { - "Karthus": "55.56%", - "Nilah": "53.13%", - "Miss Fortune": "52.07%", - "Hwei": "51.72%", - "Vayne": "51.31%", - "Sivir": "50.91%", - "Draven": "50.63%", - "Tristana": "50.00%" + "Tristana": "54.41%", + "Ashe": "52.79%", + "Twisted Fate": "52.57%", + "Jinx": "52.21%", + "Miss Fortune": "51.67%", + "Kalista": "51.02%", + "Twitch": "50.19%", + "Caitlyn": "50.15%", + "Zeri": "50.00%" } }, { - "champion_name": "Karma", + "champion_name": "Lux", "synergy": { - "Draven": "53.57%", - "Vayne": "51.30%", - "Jhin": "50.49%", - "Ezreal": "50.43%", - "Tristana": "50.42%" + "Ziggs": "57.14%", + "Twisted Fate": "56.82%", + "Samira": "52.63%", + "Senna": "52.05%", + "Ezreal": "50.65%", + "Nilah": "50.00%" } }, { - "champion_name": "Swain", + "champion_name": "Seraphine", "synergy": { - "Tristana": "64.29%", - "Karthus": "54.55%", - "Vayne": "54.01%", - "Jhin": "53.18%", - "Twitch": "50.70%", - "Nilah": "50.00%", - "Ziggs": "50.00%" + "Lucian": "61.54%", + "Tristana": "61.54%", + "Vayne": "58.90%", + "Varus": "57.69%", + "Jinx": "56.25%", + "Miss Fortune": "55.32%", + "Senna": "51.51%", + "Ezreal": "50.83%", + "Samira": "50.00%", + "Karthus": "50.00%" } }, { - "champion_name": "Pantheon", + "champion_name": "Yuumi", "synergy": { - "Miss Fortune": "63.41%", - "Seraphine": "56.25%", - "Yasuo": "54.55%", - "Swain": "52.63%", - "Varus": "52.05%", - "Zeri": "50.00%" + "Rammus": "64.71%", + "Jinx": "55.32%", + "Vayne": "54.26%", + "Samira": "53.57%", + "Lucian": "53.52%", + "Tristana": "53.33%", + "Twitch": "53.31%", + "Jhin": "50.00%", + "Xayah": "50.00%" } }, { - "champion_name": "Yuumi", + "champion_name": "Twitch", "synergy": { - "Seraphine": "66.67%", - "Yasuo": "53.25%", - "Tristana": "52.55%", - "Vayne": "51.74%", - "Zeri": "51.45%", - "Twitch": "50.41%" + "Karma": "75.00%", + "Lucian": "66.67%", + "Cassiopeia": "66.67%", + "Janna": "66.67%", + "Aphelios": "66.67%", + "Jhin": "61.90%", + "Zeri": "60.87%", + "Varus": "60.00%", + "Kalista": "57.14%", + "Xayah": "53.85%", + "Ezreal": "53.52%", + "Vayne": "53.19%", + "Sivir": "50.00%", + "Jinx": "50.00%", + "Lulu": "50.00%" } }, { "champion_name": "Hwei", "synergy": { - "Nilah": "51.61%" + "Lucian": "57.14%", + "Tristana": "54.55%", + "Swain": "53.85%", + "Miss Fortune": "51.85%", + "Seraphine": "51.02%" } } ] \ No newline at end of file diff --git a/src/personal_finance/.txt b/src/personal_finance/.txt new file mode 100755 index 0000000..e69de29 diff --git a/src/personal_finance/fin/scrape.py b/src/personal_finance/fin/scrape.py new file mode 100755 index 0000000..6998d00 --- /dev/null +++ b/src/personal_finance/fin/scrape.py @@ -0,0 +1,232 @@ +import pandas as pd +import os +from io import StringIO +import json +import openpyxl + +#simplified working version + +# Define the directory containing the CSV files +directory = '/home/eikov/fin/OG-CSV' + +# Define the pattern for file names +file_pattern = '{}{}.csv' # {} will be replaced by the file number and +/- for the file type + +# Initialize empty dictionaries to store dataframes for each month +spending_dfs = {} +income_dfs = {} + +# Define the filters +filter1 = "KOVALIŪNAS EINARAS" +filter2 = "Einaras Kovaliūnas" # Add your second filter value here + +# Function to extract first 4 words from a string +def extract_first_4_words(text): + if pd.isna(text): # Check if the value is NaN + return "" # Return an empty string if NaN + words = text.split()[:4] + return ' '.join(words) + + +# Iterate over the file numbers from 1 to 12 +for file_num in range(1, 13): + month = str(file_num) + spending_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + income_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + for file_type in ['+', '-']: + filename = file_pattern.format(file_num, file_type) + filepath = os.path.join(directory, filename) + if os.path.exists(filepath): + print(f"Processing file: {filename}") + # Initialize an empty list to store the valid lines of the CSV file + valid_lines = [] + skipped_rows = [] + + # Read the CSV file line by line + with open(filepath, 'r', encoding='utf-8') as file: + for i, line in enumerate(file): + if i < 2: # Skip the first two lines (first row and header row) + continue + try: + # Attempt to parse the line as CSV + pd.read_csv(StringIO(line), header=None, sep=';') # Use header=None and specify separator as ';' + valid_lines.append(line) # If successful, add the line to the list of valid lines + except pd.errors.ParserError: + print(f"Skipped line {i+1} in file {filename} due to ParserError: {line.strip()}") + skipped_rows.append((i+1, line.strip())) # Store the line number and the content of the skipped row + + # Read the valid lines into a DataFrame, skipping the header line + bank_data = None + try: + bank_data = pd.read_csv(StringIO(''.join(valid_lines)), header=None, sep=';') + except pd.errors.ParserError: + print(f"Encountered an error while parsing file {filename}. Skipping problematic rows.") + if not valid_lines: + print("No valid data read due to errors.") + continue + + # If data was successfully read, extract specific columns and apply filter + if bank_data is not None: + bank_data = bank_data[[1, 3, 4, 9]] # Extract only desired columns + + # Replace empty values with NaN + bank_data.replace('-', pd.NA, inplace=True) + + # Rename columns + bank_data.columns = ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"] + + # Apply filters + bank_data = bank_data[(bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter1) & + (bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter2)] + + # Filter out rows that are the same as column headers or contain all empty values + bank_data = bank_data[~bank_data.apply(lambda row: all(row == ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]), axis=1)] + + # Extract first 4 words from specified columns + bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] = bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"].apply(extract_first_4_words) + bank_data["MOKĖJIMO PASKIRTIS"] = bank_data["MOKĖJIMO PASKIRTIS"].apply(extract_first_4_words) + + # Append the DataFrame to the appropriate dictionary based on file type + if file_type == '+': + income_dfs[month] = bank_data + elif file_type == '-': + spending_dfs[month] = bank_data + else: + print(f"No valid data read from file {filename}.") + + # Display the DataFrame + if file_type == '+': + print(f"\nIncome DataFrame for month {month}:") + print(income_dfs[month]) + elif file_type == '-': + print(f"\nSpending DataFrame for month {month}:") + print(spending_dfs[month]) + + # Specify the folder path + output_folder_income = "/home/eikov/fin/income" + output_folder_spending = "/home/eikov/fin/spendings" + + # Create the folder if it doesn't exist + os.makedirs(output_folder_income, exist_ok=True) + os.makedirs(output_folder_spending, exist_ok=True) + + # Save DataFrames to JSON files with UTF-8 encoding + income_json_filename = os.path.join( output_folder_income, f"income_month_{month}.json") + spending_json_filename = os.path.join(output_folder_spending, f"spending_month_{month}.json") + + # Save DataFrames to Excel files + income_excel_filename = os.path.join( output_folder_income, f"income_month_{month}.xlsx") + spending_excel_filename = os.path.join(output_folder_spending, f"spending_month_{month}.xlsx") + + # Drop rows with NaN values + income_dfs[month].dropna(inplace=True) + spending_dfs[month].dropna(inplace=True) + + # Convert DataFrames to JSON strings + income_json_str = income_dfs[month].to_json(orient='records', lines=True, default_handler=str) + spending_json_str = spending_dfs[month].to_json(orient='records', lines=True, default_handler=str) + """ + # Write JSON strings to files with UTF-8 encoding + with open(income_json_filename, 'w+', encoding='utf-8') as income_file: + for line in income_json_str.split('\n'): + if line.strip(): # Check if line is not empty + income_file.write(line.strip() + '\n') + + with open(spending_json_filename, 'w+', encoding='utf-8') as spending_file: + for line in spending_json_str.split('\n'): + if line.strip(): # Check if line is not empty + spending_file.write(line.strip() + '\n') + """ + # Save DataFrames to Excel files + income_dfs[month].to_excel(income_excel_filename, index=False) + spending_dfs[month].to_excel(spending_excel_filename, index=False) + + print(f"DataFrames saved to JSON files: {income_json_filename}, {spending_json_filename}") + print(f"DataFrames saved to Excel files: {income_excel_filename}, {spending_excel_filename}") + + # Display skipped rows + if skipped_rows: + print(f"\nSkipped rows in file {filename}:") + for row_num, row_content in skipped_rows: + print(f"Row {row_num}: {row_content}") + else: + print(f"File {filename} not found. Skipping...") + +print("All files processed.") + + + +#Merge spending seperate xlsx files to one +directory = '/home/eikov/fin/spendings' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_spending_data.xlsx") +merged_workbook.save(output_filename) + + +#Merge income seperate xlsx files to one +directory = '/home/eikov/fin/income' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_income_data.xlsx") +merged_workbook.save(output_filename) \ No newline at end of file diff --git a/src/personal_finance/pandamat1.0.py b/src/personal_finance/pandamat1.0.py new file mode 100644 index 0000000..da52511 --- /dev/null +++ b/src/personal_finance/pandamat1.0.py @@ -0,0 +1,28 @@ +import pandas as pd +import matplotlib.pyplot as plt + +# Load the Totals sheet +file_path = '/home/eikov/fin/analysis/Manually-polished/2025/Galutinis_spendings.xlsx' # change filename +df = pd.read_excel(file_path, sheet_name='Totals') + +# Ensure SUMA is numeric (clean up bad values) +df['SUMA'] = pd.to_numeric(df['SUMA'], errors='coerce') # convert non-numeric to NaN +df = df.dropna(subset=['SUMA']) # drop rows where SUMA couldn't be converted + +# Group by Category (or Subcategory) +grouped = df.groupby('Category')['SUMA'].sum().sort_values(ascending=False) + +# Optional: filter out zero or very small values +grouped = grouped[grouped > 0] + +# Plot +plt.figure(figsize=(8, 8)) +grouped.plot.pie(autopct='%1.1f%%', startangle=90) +plt.title('Spending Distribution by Category') +plt.ylabel('') +plt.tight_layout() + +# Save pie chart +output_path = '/home/eikov/fin/analysis/spending_pie_by_category.png' +plt.savefig(output_path) +print(f"Saved pie chart to: {output_path}") diff --git a/src/personal_finance/pandamat1.1.py b/src/personal_finance/pandamat1.1.py new file mode 100644 index 0000000..1042f4c --- /dev/null +++ b/src/personal_finance/pandamat1.1.py @@ -0,0 +1,43 @@ +import pandas as pd +import matplotlib.pyplot as plt +from openpyxl import load_workbook +from openpyxl.drawing.image import Image as OpenpyxlImage + +#basic + +# File paths +excel_file = '/home/eikov/fin/analysis/Manually-polished/2025/Galutinis_spendings.xlsx' # Replace with actual file name +chart_image_path = '/home/eikov/fin/analysis/Manually-polished/2025/spending_pie_by_category.png' + +# 1. Load data from Totals sheet +df = pd.read_excel(excel_file, sheet_name='Totals') + +# 2. Clean and group +df['SUMA'] = pd.to_numeric(df['SUMA'], errors='coerce') +df = df.dropna(subset=['SUMA']) +grouped = df.groupby('Category')['SUMA'].sum().sort_values(ascending=False) +grouped = grouped[grouped > 0] + +# 3. Create and save pie chart +plt.figure(figsize=(8, 8)) +grouped.plot.pie(autopct='%1.1f%%', startangle=90) +plt.title('Spending Distribution by Category') +plt.ylabel('') +plt.tight_layout() +plt.savefig(chart_image_path) +plt.close() + +# 4. Insert image into a new sheet in the Excel file +workbook = load_workbook(excel_file) +if 'Analysis' in workbook.sheetnames: + del workbook['Analysis'] # remove if it already exists (optional) +sheet = workbook.create_sheet('Analysis') + +# Insert image +img = OpenpyxlImage(chart_image_path) +img.anchor = 'A1' # Position in the sheet +sheet.add_image(img) + +# Save workbook +workbook.save(excel_file) +print(f"✅ Pie chart inserted into 'Analysis' sheet of: {excel_file}") diff --git a/src/personal_finance/pandamat1.2.py b/src/personal_finance/pandamat1.2.py new file mode 100644 index 0000000..37dca65 --- /dev/null +++ b/src/personal_finance/pandamat1.2.py @@ -0,0 +1,84 @@ +import pandas as pd +import matplotlib.pyplot as plt +from openpyxl import load_workbook +from openpyxl.drawing.image import Image as OpenpyxlImage + +#ok + +# File paths +excel_file = '/home/eikov/fin/analysis/Manually-polished/2025/Galutinis_spendings.xlsx' # Replace with actual file name +chart_image_path = '/home/eikov/fin/analysis/Manually-polished/2025/spending_pie_by_category.png' + +# 1. Load data from Totals sheet +df = pd.read_excel(excel_file, sheet_name='Totals') + +# 2. Clean and group +df['SUMA'] = pd.to_numeric(df['SUMA'], errors='coerce') +df = df.dropna(subset=['SUMA']) +grouped = df.groupby('Category')['SUMA'].sum().sort_values(ascending=False) +grouped = grouped[grouped > 0] + + + + +# 3. Create and save pie chart +# Group by Category and Subcategory +df['SUMA'] = pd.to_numeric(df['SUMA'], errors='coerce') +df = df.dropna(subset=['SUMA']) +grouped = df.groupby(['Category', 'Subcategory'])['SUMA'].sum().sort_values(ascending=False) + +# Filter small values if chart gets too crowded (optional) +grouped = grouped[grouped > 0] + +# Build custom labels: "Category: Subcategory" +labels = [f"{cat}: {sub}" for cat, sub in grouped.index] + +# Plot with smaller font +plt.figure(figsize=(10, 10)) +patches, texts, autotexts = plt.pie( + grouped.values, + labels=labels, + autopct='%1.1f%%', + startangle=90, + textprops={'fontsize': 8} +) +plt.title('Spending Breakdown by Category and Subcategory', fontsize=12) +plt.tight_layout() + +# stacking small text +plt.figure(figsize=(10, 10)) +patches, texts, autotexts = plt.pie( + grouped.values, + labels=None, # Don't show labels on the pie + autopct='%1.1f%%', + startangle=90, + textprops={'fontsize': 8} +) + +# stacking small text: Add legend outside the pie +plt.legend(patches, labels, loc='center left', bbox_to_anchor=(1, 0.5), fontsize=8) +plt.title('Spending Breakdown by Category and Subcategory', fontsize=12) +plt.tight_layout() + + + + +# Save image +chart_image_path = '/home/eikov/fin/analysis/spending_pie_by_category_sub.png' +plt.savefig(chart_image_path) +plt.close() + +# 4. Insert image into a new sheet in the Excel file +workbook = load_workbook(excel_file) +if 'Analysis' in workbook.sheetnames: + del workbook['Analysis'] # remove if it already exists (optional) +sheet = workbook.create_sheet('Analysis') + +# Insert image +img = OpenpyxlImage(chart_image_path) +img.anchor = 'A1' # Position in the sheet +sheet.add_image(img) + +# Save workbook +workbook.save(excel_file) +print(f"✅ Pie chart inserted into 'Analysis' sheet of: {excel_file}") diff --git a/src/personal_finance/pandamat1.3.py b/src/personal_finance/pandamat1.3.py new file mode 100644 index 0000000..550fd46 --- /dev/null +++ b/src/personal_finance/pandamat1.3.py @@ -0,0 +1,73 @@ +import pandas as pd +import matplotlib.pyplot as plt +from openpyxl import load_workbook +from openpyxl.drawing.image import Image as ExcelImage + +#good + +# === Load data === +excel_file = "/home/eikov/fin/analysis/Manually-polished/2025/Galutinis_spendings.xlsx" # <-- Replace with your actual file +sheet_name = "Totals" + +df = pd.read_excel(excel_file, sheet_name=sheet_name) + +# === Group by Category and Subcategory === +grouped = df.groupby(['Category', 'Subcategory'], as_index=False)['SUMA'].sum() + +# Remove rows with zero or negative sums (optional) +grouped = grouped[grouped['SUMA'] > 0] + +# Sort by SUMA descending +grouped = grouped.sort_values(by='SUMA', ascending=False) + +# === Create pie chart === +fig, ax = plt.subplots(figsize=(8, 8)) + +# Pie chart data +sizes = grouped['SUMA'] +labels_raw = grouped[['Category', 'Subcategory']].agg(' – '.join, axis=1) + +# Plot pie and get returned patches +patches, texts, autotexts = ax.pie( + sizes, + autopct='%1.1f%%', + startangle=90 +) + +# Create legend with formatted labels including % +total = sizes.sum() +percentages = (sizes / total * 100).round(1) +legend_labels = [f"{p:.1f}% – {cat}" for p, cat in zip(percentages, labels_raw)] + +# Sort legend entries by percentages +sorted_indices = percentages.argsort()[::-1] +sorted_labels = [legend_labels[i] for i in sorted_indices] +sorted_patches = [patches[i] for i in sorted_indices] + +# Display legend +ax.legend(sorted_patches, sorted_labels, loc="center left", bbox_to_anchor=(1, 0.5), fontsize='small') + +# Save figure +chart_path = "output_chart.png" +plt.tight_layout() +plt.savefig(chart_path) +plt.close() + +# === Insert chart into Excel in sheet "Analysis" === +wb = load_workbook(excel_file) + +# Create or get the "Analysis" sheet +if "Analysis" in wb.sheetnames: + ws = wb["Analysis"] +else: + ws = wb.create_sheet("Analysis") + +# Add image to worksheet +img = ExcelImage(chart_path) +img.anchor = "A1" +ws.add_image(img) + +# Save updated Excel file +wb.save(excel_file) + +print("Chart generated and embedded into the 'Analysis' sheet.") diff --git a/src/personal_finance/pandamat1.4.py b/src/personal_finance/pandamat1.4.py new file mode 100644 index 0000000..394e1dc --- /dev/null +++ b/src/personal_finance/pandamat1.4.py @@ -0,0 +1,84 @@ +import pandas as pd +import matplotlib.pyplot as plt +from openpyxl import load_workbook +from openpyxl.drawing.image import Image as ExcelImage + +#so far the best one + +# === Load data === +excel_file = "/home/eikov/fin/analysis/Manually-polished/2025/Galutinis_spendings.xlsx" # <-- Replace with your actual file +sheet_name = "Totals" + +df = pd.read_excel(excel_file, sheet_name=sheet_name) + +# === Group by Category and Subcategory === +grouped = df.groupby(['Category', 'Subcategory'], as_index=False)['SUMA'].sum() + +# Remove zero or negative values +grouped = grouped[grouped['SUMA'] > 0] + +# Sort by SUMA descending +grouped = grouped.sort_values(by='SUMA', ascending=False) + +# === Create pie chart === +fig, ax = plt.subplots(figsize=(8, 8)) + +# Pie data +sizes = grouped['SUMA'] +labels_raw = grouped[['Category', 'Subcategory']].agg(' – '.join, axis=1) + +# Pie chart +patches, texts, autotexts = ax.pie( + sizes, + autopct='%1.1f%%', + startangle=90 +) + +# Generate custom labels with % + numeric sum + category info +total = sizes.sum() +percentages = (sizes / total * 100).round(1) +sums_formatted = sizes.round(2) + +legend_labels = [ + f"{p:.1f}% – {s:.2f} – {label}" + for p, s, label in zip(percentages, sums_formatted, labels_raw) +] + +# Sort legend entries by % descending +sorted_indices = percentages.argsort()[::-1] +sorted_labels = [legend_labels[i] for i in sorted_indices] +sorted_patches = [patches[i] for i in sorted_indices] + +# Add legend +ax.legend( + sorted_patches, + sorted_labels, + loc="center left", + bbox_to_anchor=(1, 0.5), + fontsize='small' +) + +# Save the pie chart +chart_path = "output_chart.png" +plt.tight_layout() +plt.savefig(chart_path) +plt.close() + +# === Insert chart into Excel === +wb = load_workbook(excel_file) + +# Create or get the "Analysis" sheet +if "Analysis" in wb.sheetnames: + ws = wb["Analysis"] +else: + ws = wb.create_sheet("Analysis") + +# Add image +img = ExcelImage(chart_path) +img.anchor = "A1" +ws.add_image(img) + +# Save updated Excel file +wb.save(excel_file) + +print("Chart with enhanced legend saved and embedded into 'Analysis' sheet.") diff --git a/src/personal_finance/run_script.sh b/src/personal_finance/run_script.sh new file mode 100755 index 0000000..1fab98f --- /dev/null +++ b/src/personal_finance/run_script.sh @@ -0,0 +1,15 @@ +#!/bin/zsh + +# activate the virtual environment, because that's where all modules are installed, not in system python +source ~/scraping_env/bin/activate + +# Navigate to the directory where your Python script is located +cd /home/eikov/fin + +# Run your Python script +python3 scrape.py #or latest version like v2.3.py + +# optional: for better analysis create visual charts with pandas, mathplotlib +python3 pandamat.py + +chmod +x run_script.sh diff --git a/src/personal_finance/scheme.txt b/src/personal_finance/scheme.txt new file mode 100755 index 0000000..77f3d4a --- /dev/null +++ b/src/personal_finance/scheme.txt @@ -0,0 +1,17 @@ +step1: download seb monthly csv israsus +step2: rename them by month and add +(if parsing incomes) or -(if parsing spendings) +step3: enjoy clean data in xlsx , showing Date/Sum/Moketojas/Paskirtis +step4: edit summed-strings-spendings.xlsx to assign each row sub-category and category, rename to a) cat-income.xlsx b) cat-spendings.xlsx then rerun .py again +step5: run pandamat.py for visuals, charts, analysis + +Scheme +Step 0 DONE: Download monthly banko israsus +FUNCTIONALITY 1 DONE: Extract wanted columns: "DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS" .... and clean data to make it more neat, understandable. f +ilter-out own transactions + +TBD: +FUNCTIONALITY 2: add new column "Category" +FUNCTIONALITY 3: Excel calculations, analysis, pie chart and projections +FUNCTIONALITY 4 DONE: Extract incomes and clean data: filter-out own transactions +Extra: Calculate avg of 3 months fixed costs spending +Extra: Compare this realistic report with realistic numbers to my manual "personal finance" balance sheet diff --git a/src/personal_finance/v1.7.py b/src/personal_finance/v1.7.py new file mode 100755 index 0000000..42f7430 --- /dev/null +++ b/src/personal_finance/v1.7.py @@ -0,0 +1,143 @@ +import pandas as pd +import os +from io import StringIO +import json + +# Define the directory containing the CSV files +directory = 'OGCSV/' + +# Define the pattern for file names +file_pattern = '{}{}.csv' # {} will be replaced by the file number and +/- for the file type + +# Initialize empty dictionaries to store dataframes for each month +spending_dfs = {} +income_dfs = {} + +# Define the filters +filter1 = "KOVALIŪNAS EINARAS" +filter2 = "Einaras Kovaliūnas" # Add your second filter value here + +# Function to extract first 4 words from a string +def extract_first_4_words(text): + if pd.isna(text): # Check if the value is NaN + return "" # Return an empty string if NaN + words = text.split()[:4] + return ' '.join(words) + + +# Iterate over the file numbers from 1 to 12 +for file_num in range(1, 13): + month = str(file_num) + spending_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + income_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + for file_type in ['+', '-']: + filename = file_pattern.format(file_num, file_type) + filepath = os.path.join(directory, filename) + if os.path.exists(filepath): + print(f"Processing file: {filename}") + # Initialize an empty list to store the valid lines of the CSV file + valid_lines = [] + skipped_rows = [] + + # Read the CSV file line by line + with open(filepath, 'r', encoding='utf-8') as file: + for i, line in enumerate(file): + if i < 2: # Skip the first two lines (first row and header row) + continue + try: + # Attempt to parse the line as CSV + pd.read_csv(StringIO(line), header=None, sep=';') # Use header=None and specify separator as ';' + valid_lines.append(line) # If successful, add the line to the list of valid lines + except pd.errors.ParserError: + print(f"Skipped line {i+1} in file {filename} due to ParserError: {line.strip()}") + skipped_rows.append((i+1, line.strip())) # Store the line number and the content of the skipped row + + # Read the valid lines into a DataFrame, skipping the header line + bank_data = None + try: + bank_data = pd.read_csv(StringIO(''.join(valid_lines)), header=None, sep=';') + except pd.errors.ParserError: + print(f"Encountered an error while parsing file {filename}. Skipping problematic rows.") + if not valid_lines: + print("No valid data read due to errors.") + continue + + # If data was successfully read, extract specific columns and apply filter + if bank_data is not None: + bank_data = bank_data[[1, 3, 4, 9]] # Extract only desired columns + + # Replace empty values with NaN + bank_data.replace('-', pd.NA, inplace=True) + + # Rename columns + bank_data.columns = ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"] + + # Apply filters + bank_data = bank_data[(bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter1) & + (bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter2)] + + # Filter out rows that are the same as column headers or contain all empty values + bank_data = bank_data[~bank_data.apply(lambda row: all(row == ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]), axis=1)] + + # Extract first 4 words from specified columns + bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] = bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"].apply(extract_first_4_words) + bank_data["MOKĖJIMO PASKIRTIS"] = bank_data["MOKĖJIMO PASKIRTIS"].apply(extract_first_4_words) + + # Append the DataFrame to the appropriate dictionary based on file type + if file_type == '+': + income_dfs[month] = bank_data + elif file_type == '-': + spending_dfs[month] = bank_data + else: + print(f"No valid data read from file {filename}.") + + # Display the DataFrame + if file_type == '+': + print(f"\nIncome DataFrame for month {month}:") + print(income_dfs[month]) + elif file_type == '-': + print(f"\nSpending DataFrame for month {month}:") + print(spending_dfs[month]) + # Save DataFrames to JSON files with UTF-8 encoding + income_json_filename = f"income_month_{month}.json" + spending_json_filename = f"spending_month_{month}.json" + + # Save DataFrames to Excel files + income_excel_filename = f"income_month_{month}.xlsx" + spending_excel_filename = f"spending_month_{month}.xlsx" + + # Drop rows with NaN values + income_dfs[month].dropna(inplace=True) + spending_dfs[month].dropna(inplace=True) + + # Convert DataFrames to JSON strings + income_json_str = income_dfs[month].to_json(orient='records', lines=True, default_handler=str) + spending_json_str = spending_dfs[month].to_json(orient='records', lines=True, default_handler=str) + + # Write JSON strings to files with UTF-8 encoding + with open(income_json_filename, 'w', encoding='utf-8') as income_file: + for line in income_json_str.split('\n'): + if line.strip(): # Check if line is not empty + income_file.write(line.strip() + '\n') + + with open(spending_json_filename, 'w', encoding='utf-8') as spending_file: + for line in spending_json_str.split('\n'): + if line.strip(): # Check if line is not empty + spending_file.write(line.strip() + '\n') + + # Save DataFrames to Excel files + income_dfs[month].to_excel(income_excel_filename, index=False) + spending_dfs[month].to_excel(spending_excel_filename, index=False) + + print(f"DataFrames saved to JSON files: {income_json_filename}, {spending_json_filename}") + print(f"DataFrames saved to Excel files: {income_excel_filename}, {spending_excel_filename}") + + # Display skipped rows + if skipped_rows: + print(f"\nSkipped rows in file {filename}:") + for row_num, row_content in skipped_rows: + print(f"Row {row_num}: {row_content}") + else: + print(f"File {filename} not found. Skipping...") + +print("All files processed.") diff --git a/src/personal_finance/v1.8.py b/src/personal_finance/v1.8.py new file mode 100755 index 0000000..9584e84 --- /dev/null +++ b/src/personal_finance/v1.8.py @@ -0,0 +1,152 @@ +import pandas as pd +import os +from io import StringIO +import json + +# Define the directory containing the CSV files +directory = '/home/eikov/fin/OG-CSV' + +# Define the pattern for file names +file_pattern = '{}{}.csv' # {} will be replaced by the file number and +/- for the file type + +# Initialize empty dictionaries to store dataframes for each month +spending_dfs = {} +income_dfs = {} + +# Define the filters +filter1 = "KOVALIŪNAS EINARAS" +filter2 = "Einaras Kovaliūnas" # Add your second filter value here + +# Function to extract first 4 words from a string +def extract_first_4_words(text): + if pd.isna(text): # Check if the value is NaN + return "" # Return an empty string if NaN + words = text.split()[:4] + return ' '.join(words) + + +# Iterate over the file numbers from 1 to 12 +for file_num in range(1, 13): + month = str(file_num) + spending_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + income_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + for file_type in ['+', '-']: + filename = file_pattern.format(file_num, file_type) + filepath = os.path.join(directory, filename) + if os.path.exists(filepath): + print(f"Processing file: {filename}") + # Initialize an empty list to store the valid lines of the CSV file + valid_lines = [] + skipped_rows = [] + + # Read the CSV file line by line + with open(filepath, 'r', encoding='utf-8') as file: + for i, line in enumerate(file): + if i < 2: # Skip the first two lines (first row and header row) + continue + try: + # Attempt to parse the line as CSV + pd.read_csv(StringIO(line), header=None, sep=';') # Use header=None and specify separator as ';' + valid_lines.append(line) # If successful, add the line to the list of valid lines + except pd.errors.ParserError: + print(f"Skipped line {i+1} in file {filename} due to ParserError: {line.strip()}") + skipped_rows.append((i+1, line.strip())) # Store the line number and the content of the skipped row + + # Read the valid lines into a DataFrame, skipping the header line + bank_data = None + try: + bank_data = pd.read_csv(StringIO(''.join(valid_lines)), header=None, sep=';') + except pd.errors.ParserError: + print(f"Encountered an error while parsing file {filename}. Skipping problematic rows.") + if not valid_lines: + print("No valid data read due to errors.") + continue + + # If data was successfully read, extract specific columns and apply filter + if bank_data is not None: + bank_data = bank_data[[1, 3, 4, 9]] # Extract only desired columns + + # Replace empty values with NaN + bank_data.replace('-', pd.NA, inplace=True) + + # Rename columns + bank_data.columns = ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"] + + # Apply filters + bank_data = bank_data[(bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter1) & + (bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter2)] + + # Filter out rows that are the same as column headers or contain all empty values + bank_data = bank_data[~bank_data.apply(lambda row: all(row == ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]), axis=1)] + + # Extract first 4 words from specified columns + bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] = bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"].apply(extract_first_4_words) + bank_data["MOKĖJIMO PASKIRTIS"] = bank_data["MOKĖJIMO PASKIRTIS"].apply(extract_first_4_words) + + # Append the DataFrame to the appropriate dictionary based on file type + if file_type == '+': + income_dfs[month] = bank_data + elif file_type == '-': + spending_dfs[month] = bank_data + else: + print(f"No valid data read from file {filename}.") + + # Display the DataFrame + if file_type == '+': + print(f"\nIncome DataFrame for month {month}:") + print(income_dfs[month]) + elif file_type == '-': + print(f"\nSpending DataFrame for month {month}:") + print(spending_dfs[month]) + + # Specify the folder path + output_folder_income = "/home/eikov/fin/income" + output_folder_spending = "/home/eikov/fin/spendings" + + # Create the folder if it doesn't exist + os.makedirs(output_folder_income, exist_ok=True) + os.makedirs(output_folder_spending, exist_ok=True) + + # Save DataFrames to JSON files with UTF-8 encoding + income_json_filename = os.path.join( output_folder_income, f"income_month_{month}.json") + spending_json_filename = os.path.join(output_folder_spending, f"spending_month_{month}.json") + + # Save DataFrames to Excel files + income_excel_filename = os.path.join( output_folder_income, f"income_month_{month}.xlsx") + spending_excel_filename = os.path.join(output_folder_spending, f"spending_month_{month}.xlsx") + + # Drop rows with NaN values + income_dfs[month].dropna(inplace=True) + spending_dfs[month].dropna(inplace=True) + + # Convert DataFrames to JSON strings + income_json_str = income_dfs[month].to_json(orient='records', lines=True, default_handler=str) + spending_json_str = spending_dfs[month].to_json(orient='records', lines=True, default_handler=str) + """ + # Write JSON strings to files with UTF-8 encoding + with open(income_json_filename, 'w+', encoding='utf-8') as income_file: + for line in income_json_str.split('\n'): + if line.strip(): # Check if line is not empty + income_file.write(line.strip() + '\n') + + with open(spending_json_filename, 'w+', encoding='utf-8') as spending_file: + for line in spending_json_str.split('\n'): + if line.strip(): # Check if line is not empty + spending_file.write(line.strip() + '\n') + """ + # Save DataFrames to Excel files + income_dfs[month].to_excel(income_excel_filename, index=False) + spending_dfs[month].to_excel(spending_excel_filename, index=False) + + print(f"DataFrames saved to JSON files: {income_json_filename}, {spending_json_filename}") + print(f"DataFrames saved to Excel files: {income_excel_filename}, {spending_excel_filename}") + + # Display skipped rows + if skipped_rows: + print(f"\nSkipped rows in file {filename}:") + for row_num, row_content in skipped_rows: + print(f"Row {row_num}: {row_content}") + else: + print(f"File {filename} not found. Skipping...") + +print("All files processed.") diff --git a/src/personal_finance/v1.9.py b/src/personal_finance/v1.9.py new file mode 100755 index 0000000..6318e8b --- /dev/null +++ b/src/personal_finance/v1.9.py @@ -0,0 +1,230 @@ +import pandas as pd +import os +from io import StringIO +import json +import openpyxl + +# Define the directory containing the CSV files +directory = '/home/eikov/fin/OG-CSV' + +# Define the pattern for file names +file_pattern = '{}{}.csv' # {} will be replaced by the file number and +/- for the file type + +# Initialize empty dictionaries to store dataframes for each month +spending_dfs = {} +income_dfs = {} + +# Define the filters +filter1 = "KOVALIŪNAS EINARAS" +filter2 = "Einaras Kovaliūnas" # Add your second filter value here + +# Function to extract first 4 words from a string +def extract_first_4_words(text): + if pd.isna(text): # Check if the value is NaN + return "" # Return an empty string if NaN + words = text.split()[:4] + return ' '.join(words) + + +# Iterate over the file numbers from 1 to 12 +for file_num in range(1, 13): + month = str(file_num) + spending_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + income_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + for file_type in ['+', '-']: + filename = file_pattern.format(file_num, file_type) + filepath = os.path.join(directory, filename) + if os.path.exists(filepath): + print(f"Processing file: {filename}") + # Initialize an empty list to store the valid lines of the CSV file + valid_lines = [] + skipped_rows = [] + + # Read the CSV file line by line + with open(filepath, 'r', encoding='utf-8') as file: + for i, line in enumerate(file): + if i < 2: # Skip the first two lines (first row and header row) + continue + try: + # Attempt to parse the line as CSV + pd.read_csv(StringIO(line), header=None, sep=';') # Use header=None and specify separator as ';' + valid_lines.append(line) # If successful, add the line to the list of valid lines + except pd.errors.ParserError: + print(f"Skipped line {i+1} in file {filename} due to ParserError: {line.strip()}") + skipped_rows.append((i+1, line.strip())) # Store the line number and the content of the skipped row + + # Read the valid lines into a DataFrame, skipping the header line + bank_data = None + try: + bank_data = pd.read_csv(StringIO(''.join(valid_lines)), header=None, sep=';') + except pd.errors.ParserError: + print(f"Encountered an error while parsing file {filename}. Skipping problematic rows.") + if not valid_lines: + print("No valid data read due to errors.") + continue + + # If data was successfully read, extract specific columns and apply filter + if bank_data is not None: + bank_data = bank_data[[1, 3, 4, 9]] # Extract only desired columns + + # Replace empty values with NaN + bank_data.replace('-', pd.NA, inplace=True) + + # Rename columns + bank_data.columns = ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"] + + # Apply filters + bank_data = bank_data[(bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter1) & + (bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter2)] + + # Filter out rows that are the same as column headers or contain all empty values + bank_data = bank_data[~bank_data.apply(lambda row: all(row == ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]), axis=1)] + + # Extract first 4 words from specified columns + bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] = bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"].apply(extract_first_4_words) + bank_data["MOKĖJIMO PASKIRTIS"] = bank_data["MOKĖJIMO PASKIRTIS"].apply(extract_first_4_words) + + # Append the DataFrame to the appropriate dictionary based on file type + if file_type == '+': + income_dfs[month] = bank_data + elif file_type == '-': + spending_dfs[month] = bank_data + else: + print(f"No valid data read from file {filename}.") + + # Display the DataFrame + if file_type == '+': + print(f"\nIncome DataFrame for month {month}:") + print(income_dfs[month]) + elif file_type == '-': + print(f"\nSpending DataFrame for month {month}:") + print(spending_dfs[month]) + + # Specify the folder path + output_folder_income = "/home/eikov/fin/income" + output_folder_spending = "/home/eikov/fin/spendings" + + # Create the folder if it doesn't exist + os.makedirs(output_folder_income, exist_ok=True) + os.makedirs(output_folder_spending, exist_ok=True) + + # Save DataFrames to JSON files with UTF-8 encoding + income_json_filename = os.path.join( output_folder_income, f"income_month_{month}.json") + spending_json_filename = os.path.join(output_folder_spending, f"spending_month_{month}.json") + + # Save DataFrames to Excel files + income_excel_filename = os.path.join( output_folder_income, f"income_month_{month}.xlsx") + spending_excel_filename = os.path.join(output_folder_spending, f"spending_month_{month}.xlsx") + + # Drop rows with NaN values + income_dfs[month].dropna(inplace=True) + spending_dfs[month].dropna(inplace=True) + + # Convert DataFrames to JSON strings + income_json_str = income_dfs[month].to_json(orient='records', lines=True, default_handler=str) + spending_json_str = spending_dfs[month].to_json(orient='records', lines=True, default_handler=str) + """ + # Write JSON strings to files with UTF-8 encoding + with open(income_json_filename, 'w+', encoding='utf-8') as income_file: + for line in income_json_str.split('\n'): + if line.strip(): # Check if line is not empty + income_file.write(line.strip() + '\n') + + with open(spending_json_filename, 'w+', encoding='utf-8') as spending_file: + for line in spending_json_str.split('\n'): + if line.strip(): # Check if line is not empty + spending_file.write(line.strip() + '\n') + """ + # Save DataFrames to Excel files + income_dfs[month].to_excel(income_excel_filename, index=False) + spending_dfs[month].to_excel(spending_excel_filename, index=False) + + print(f"DataFrames saved to JSON files: {income_json_filename}, {spending_json_filename}") + print(f"DataFrames saved to Excel files: {income_excel_filename}, {spending_excel_filename}") + + # Display skipped rows + if skipped_rows: + print(f"\nSkipped rows in file {filename}:") + for row_num, row_content in skipped_rows: + print(f"Row {row_num}: {row_content}") + else: + print(f"File {filename} not found. Skipping...") + +print("All files processed.") + + + +#Merge spending seperate xlsx files to one +directory = '/home/eikov/fin/spendings' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_spending_data.xlsx") +merged_workbook.save(output_filename) + + +#Merge income seperate xlsx files to one +directory = '/home/eikov/fin/income' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_income_data.xlsx") +merged_workbook.save(output_filename) \ No newline at end of file diff --git a/src/personal_finance/v2.0.py b/src/personal_finance/v2.0.py new file mode 100755 index 0000000..ac32c08 --- /dev/null +++ b/src/personal_finance/v2.0.py @@ -0,0 +1,309 @@ +import pandas as pd +import os +from io import StringIO +import json +import openpyxl + +# Define the directory containing the CSV files +directory = '/home/eikov/fin/OG-CSV' + +# Define the pattern for file names +file_pattern = '{}{}.csv' # {} will be replaced by the file number and +/- for the file type + +# Initialize empty dictionaries to store dataframes for each month +spending_dfs = {} +income_dfs = {} + +# Define the filters +filter1 = "KOVALIŪNAS EINARAS" +filter2 = "Einaras Kovaliūnas" # Add your second filter value here + +# Function to extract first 4 words from a string +def extract_first_4_words(text): + if pd.isna(text): # Check if the value is NaN + return "" # Return an empty string if NaN + words = text.split()[:4] + return ' '.join(words) + + +# Iterate over the file numbers from 1 to 12 +for file_num in range(1, 13): + month = str(file_num) + spending_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + income_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + for file_type in ['+', '-']: + filename = file_pattern.format(file_num, file_type) + filepath = os.path.join(directory, filename) + if os.path.exists(filepath): + print(f"Processing file: {filename}") + # Initialize an empty list to store the valid lines of the CSV file + valid_lines = [] + skipped_rows = [] + + # Read the CSV file line by line + with open(filepath, 'r', encoding='utf-8') as file: + for i, line in enumerate(file): + if i < 2: # Skip the first two lines (first row and header row) + continue + try: + # Attempt to parse the line as CSV + pd.read_csv(StringIO(line), header=None, sep=';') # Use header=None and specify separator as ';' + valid_lines.append(line) # If successful, add the line to the list of valid lines + except pd.errors.ParserError: + print(f"Skipped line {i+1} in file {filename} due to ParserError: {line.strip()}") + skipped_rows.append((i+1, line.strip())) # Store the line number and the content of the skipped row + + # Read the valid lines into a DataFrame, skipping the header line + bank_data = None + try: + bank_data = pd.read_csv(StringIO(''.join(valid_lines)), header=None, sep=';') + except pd.errors.ParserError: + print(f"Encountered an error while parsing file {filename}. Skipping problematic rows.") + if not valid_lines: + print("No valid data read due to errors.") + continue + + # If data was successfully read, extract specific columns and apply filter + if bank_data is not None: + bank_data = bank_data[[1, 3, 4, 9]] # Extract only desired columns + + # Replace empty values with NaN + bank_data.replace('-', pd.NA, inplace=True) + + # Rename columns + bank_data.columns = ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"] + + # Apply filters + bank_data = bank_data[(bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter1) & + (bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter2)] + + # Filter out rows that are the same as column headers or contain all empty values + bank_data = bank_data[~bank_data.apply(lambda row: all(row == ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]), axis=1)] + + # Extract first 4 words from specified columns + bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] = bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"].apply(extract_first_4_words) + bank_data["MOKĖJIMO PASKIRTIS"] = bank_data["MOKĖJIMO PASKIRTIS"].apply(extract_first_4_words) + + # Append the DataFrame to the appropriate dictionary based on file type + if file_type == '+': + income_dfs[month] = bank_data + elif file_type == '-': + spending_dfs[month] = bank_data + else: + print(f"No valid data read from file {filename}.") + + # Display the DataFrame + if file_type == '+': + print(f"\nIncome DataFrame for month {month}:") + print(income_dfs[month]) + elif file_type == '-': + print(f"\nSpending DataFrame for month {month}:") + print(spending_dfs[month]) + + # Specify the folder path + output_folder_income = "/home/eikov/fin/income" + output_folder_spending = "/home/eikov/fin/spendings" + + # Create the folder if it doesn't exist + os.makedirs(output_folder_income, exist_ok=True) + os.makedirs(output_folder_spending, exist_ok=True) + + # Save DataFrames to JSON files with UTF-8 encoding + income_json_filename = os.path.join( output_folder_income, f"income_month_{month}.json") + spending_json_filename = os.path.join(output_folder_spending, f"spending_month_{month}.json") + + # Save DataFrames to Excel files + income_excel_filename = os.path.join( output_folder_income, f"income_month_{month}.xlsx") + spending_excel_filename = os.path.join(output_folder_spending, f"spending_month_{month}.xlsx") + + # Drop rows with NaN values + income_dfs[month].dropna(inplace=True) + spending_dfs[month].dropna(inplace=True) + + # Convert DataFrames to JSON strings + income_json_str = income_dfs[month].to_json(orient='records', lines=True, default_handler=str) + spending_json_str = spending_dfs[month].to_json(orient='records', lines=True, default_handler=str) + """ + # Write JSON strings to files with UTF-8 encoding + with open(income_json_filename, 'w+', encoding='utf-8') as income_file: + for line in income_json_str.split('\n'): + if line.strip(): # Check if line is not empty + income_file.write(line.strip() + '\n') + + with open(spending_json_filename, 'w+', encoding='utf-8') as spending_file: + for line in spending_json_str.split('\n'): + if line.strip(): # Check if line is not empty + spending_file.write(line.strip() + '\n') + """ + # Save DataFrames to Excel files + income_dfs[month].to_excel(income_excel_filename, index=False) + spending_dfs[month].to_excel(spending_excel_filename, index=False) + + print(f"DataFrames saved to JSON files: {income_json_filename}, {spending_json_filename}") + print(f"DataFrames saved to Excel files: {income_excel_filename}, {spending_excel_filename}") + + # Display skipped rows + if skipped_rows: + print(f"\nSkipped rows in file {filename}:") + for row_num, row_content in skipped_rows: + print(f"Row {row_num}: {row_content}") + else: + print(f"File {filename} not found. Skipping...") + +print("All files processed.") + + + + + + + + +#Merge spending seperate xlsx files to one +directory = '/home/eikov/fin/spendings' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_spending_data.xlsx") +merged_workbook.save(output_filename) + + +#Merge income seperate xlsx files to one +directory = '/home/eikov/fin/income' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_income_data.xlsx") +merged_workbook.save(output_filename) + + + + + + + + + + +# finding unique strings in INCOME and categorise it +# Path to the directory containing the file +output_directory = '/home/eikov/fin/analysis/' +# Specify the filename +filename = 'merged_income_data.xlsx' +# Construct the full path +file_path = f'{output_directory}/{filename}' + +# Initialize an empty set to store unique values +unique_values = set() + +# Read the Excel file +xls = pd.ExcelFile(file_path) + +# Iterate over each sheet in the Excel file +for sheet_name in xls.sheet_names: + # Read the sheet into a DataFrame + df = pd.read_excel(xls, sheet_name) + # Check the number of columns in the DataFrame + if df.shape[1] < 4: # If the number of columns is less than 4 + print(f"Skipping sheet '{sheet_name}' because it has less than 4 columns.") + continue # Skip to the next sheet + # Extract unique values from column 3 (Python uses zero-based indexing, so the third column is indexed as 2) + unique_values.update(df.iloc[:, 2].unique()) + +# Create a DataFrame with unique values and save it to 'categories.xlsx' +unique_df = pd.DataFrame({'Unique Values': list(unique_values)}) +unique_df.to_excel(f'{output_directory}/categories_income.xlsx', index=False) + + + + + + + + +# finding unique strings in SPENDINGS and categorise it +# Path to the directory containing the file +output_directory2 = '/home/eikov/fin/analysis/' +# Specify the filename +filename2 = 'merged_spending_data.xlsx' +# Construct the full path +file_path2 = f'{output_directory2}/{filename2}' + +# Initialize an empty set to store unique values +unique_values2 = set() + +# Read the Excel file +xls = pd.ExcelFile(file_path2) + +# Iterate over each sheet in the Excel file +for sheet_name in xls.sheet_names: + # Read the sheet into a DataFrame + df = pd.read_excel(xls, sheet_name) + # Check the number of columns in the DataFrame + if df.shape[1] < 4: # If the number of columns is less than 4 + print(f"Skipping sheet '{sheet_name}' because it has less than 4 columns.") + continue # Skip to the next sheet + # Extract unique values from column 3 (Python uses zero-based indexing, so the third column is indexed as 2) + unique_values2.update(df.iloc[:, 2].unique()) + +# Create a DataFrame with unique values and save it to 'categories.xlsx' +unique_df = pd.DataFrame({'Unique Values': list(unique_values2)}) +unique_df.to_excel(f'{output_directory}/categories_spendings.xlsx', index=False) diff --git a/src/personal_finance/v2.1.py b/src/personal_finance/v2.1.py new file mode 100755 index 0000000..f922e87 --- /dev/null +++ b/src/personal_finance/v2.1.py @@ -0,0 +1,393 @@ +import pandas as pd +import os +from io import StringIO +import json +import openpyxl + +""" +This code is designed to process bank statement files and organize the data into separate DataFrames for income and spending transactions. It performs the following tasks: + +1. Reads bank statement files from a specified directory. +2. Extracts relevant information from each file, such as transaction date, amount, payer/payee name, and purpose. +3. Applies data cleaning and preprocessing steps, including removing unnecessary columns and extracting the first two words from payer/payee names and payment purposes. +4. Organizes the data into separate DataFrames for income and spending transactions, grouped by month. +5. Saves the DataFrames as JSON and Excel files in separate directories for income and spending. +6. Merges separate monthly Excel files into single "merged_income_data.xlsx" and "merged_spending_data.xlsx" files. +7. Extracts unique payer/payee names from the merged files and saves them as "categories_income.xlsx" and "categories_spendings.xlsx". +8. Allows manual categorization of payer/payee names by creating "cat-income.xlsx" and "cat-spendings.xlsx" files. +9. Merges the categorized data and analysis sheets into final "Galutinis_Income.xlsx" and "Galutinis_spendings.xlsx" files. + +The code is designed to be modular and extensible, allowing for easy integration of additional data processing or analysis steps as needed. +""" + +""" +#next: in Line 229 +modify Excel file: at the end of the entries in column "SUMA" make a formula to add sum all values of that column in order to calculate total income/spending. +or if it's not possible then simply add sum function to B80 cell. + +#next: in 8: create DF for each found excel spending(-) file(ignore the last row with total sum) and join into one DF. +now iterate each unique value and sum all "SUMA" values. if that's not possible, convert DF to set and then sum key values. +""" + + +# Define the directory containing the CSV files +directory = '/home/eikov/fin/OG-CSV' + +# Define the pattern for file names +file_pattern = '{}{}.csv' # {} will be replaced by the file number and +/- for the file type + +# Initialize empty dictionaries to store dataframes for each month +spending_dfs = {} +income_dfs = {} + +# Define the filters +filter1 = "KOVALIŪNAS EINARAS" +filter2 = "Einaras Kovaliūnas" # Add your second filter value here + + +# Function to extract first 2 words from a string +def extract_first_2_words(text): + if pd.isna(text): # Check if the value is NaN + return "" # Return an empty string if NaN + words = text.split()[:2] + return ' '.join(words) + + + +# Iterate over the file numbers from 1 to 12 +for file_num in range(1, 13): + month = str(file_num) + spending_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + income_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + for file_type in ['+', '-']: + filename = file_pattern.format(file_num, file_type) + filepath = os.path.join(directory, filename) + if os.path.exists(filepath): + print(f"Processing file: {filename}") + # Initialize an empty list to store the valid lines of the CSV file + valid_lines = [] + skipped_rows = [] + + # Read the CSV file line by line + with open(filepath, 'r', encoding='utf-8') as file: + for i, line in enumerate(file): + if i < 2: # Skip the first two lines (first row and header row) + continue + try: + # Attempt to parse the line as CSV + pd.read_csv(StringIO(line), header=None, sep=';') # Use header=None and specify separator as ';' + valid_lines.append(line) # If successful, add the line to the list of valid lines + except pd.errors.ParserError: + print(f"Skipped line {i+1} in file {filename} due to ParserError: {line.strip()}") + skipped_rows.append((i+1, line.strip())) # Store the line number and the content of the skipped row + + # Read the valid lines into a DataFrame, skipping the header line + bank_data = None + try: + bank_data = pd.read_csv(StringIO(''.join(valid_lines)), header=None, sep=';') + except pd.errors.ParserError: + print(f"Encountered an error while parsing file {filename}. Skipping problematic rows.") + if not valid_lines: + print("No valid data read due to errors.") + continue + + # If data was successfully read, extract specific columns and apply filter + if bank_data is not None: + bank_data = bank_data[[1, 3, 4, 9]] # Extract only desired columns + + # Replace empty values with NaN + bank_data.replace('-', pd.NA, inplace=True) + + # Rename columns + bank_data.columns = ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"] + + # Apply filters + bank_data = bank_data[(bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter1) & + (bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter2)] + + # Filter out rows that are the same as column headers or contain all empty values + bank_data = bank_data[~bank_data.apply(lambda row: all(row == ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]), axis=1)] + + # Extract first 4 words from specified columns + bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] = bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"].apply(extract_first_2_words) + bank_data["MOKĖJIMO PASKIRTIS"] = bank_data["MOKĖJIMO PASKIRTIS"].apply(extract_first_2_words) + + # Append the DataFrame to the appropriate dictionary based on file type + if file_type == '+': + income_dfs[month] = bank_data + elif file_type == '-': + spending_dfs[month] = bank_data + else: + print(f"No valid data read from file {filename}.") + + # Display the DataFrame + if file_type == '+': + print(f"\nIncome DataFrame for month {month}:") + print(income_dfs[month]) + elif file_type == '-': + print(f"\nSpending DataFrame for month {month}:") + print(spending_dfs[month]) + + # Specify the folder path + output_folder_income = "/home/eikov/fin/income" + output_folder_spending = "/home/eikov/fin/spendings" + + # Create the folder if it doesn't exist + os.makedirs(output_folder_income, exist_ok=True) + os.makedirs(output_folder_spending, exist_ok=True) + + # Save DataFrames to JSON files + income_json_filename = os.path.join( output_folder_income, f"income_month_{month}.json") + spending_json_filename = os.path.join(output_folder_spending, f"spending_month_{month}.json") + + # Save DataFrames to Excel files + income_excel_filename = os.path.join( output_folder_income, f"income_month_{month}.xlsx") + spending_excel_filename = os.path.join(output_folder_spending, f"spending_month_{month}.xlsx") + + # Drop rows with NaN values + income_dfs[month].dropna(inplace=True) + spending_dfs[month].dropna(inplace=True) + + # Convert DataFrames to JSON strings + income_json_str = income_dfs[month].to_json(orient='records', lines=True, default_handler=str) + spending_json_str = spending_dfs[month].to_json(orient='records', lines=True, default_handler=str) + + # Save DataFrames to Excel files + income_dfs[month].to_excel(income_excel_filename, index=False) + spending_dfs[month].to_excel(spending_excel_filename, index=False) + + print(f"DataFrames saved to JSON files: {income_json_filename}, {spending_json_filename}") + print(f"DataFrames saved to Excel files: {income_excel_filename}, {spending_excel_filename}") + + # Display skipped rows + if skipped_rows: + print(f"\nSkipped rows in file {filename}:") + for row_num, row_content in skipped_rows: + print(f"Row {row_num}: {row_content}") + else: + print(f"File {filename} not found. Skipping...") + +print("All files processed.") + + + + + + + + +#Merge spending seperate xlsx files to one +directory = '/home/eikov/fin/spendings' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_spending_data.xlsx") +merged_workbook.save(output_filename) + + +#Merge income seperate xlsx files to one +directory = '/home/eikov/fin/income' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +#additionally add at the end of the entries in column "SUMA" make a formula to add sum all values of that column in order to calculate total income/spending. +#or if it's not possible then simply add sum function(=SUM(B1:B79)) to B80 cell in each sheet +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_income_data.xlsx") +merged_workbook.save(output_filename) + + + + + + + + + + +# finding unique strings in INCOME and categorise it +# Path to the directory containing the file +output_directory = '/home/eikov/fin/analysis/' +# Specify the filename +filename = 'merged_income_data.xlsx' +# Construct the full path +file_path = f'{output_directory}/{filename}' + +# Initialize an empty set to store unique values +unique_values = set() + +# Read the Excel file +xls = pd.ExcelFile(file_path) + +# Iterate over each sheet in the Excel file +for sheet_name in xls.sheet_names: + # Read the sheet into a DataFrame + df = pd.read_excel(xls, sheet_name) + # Check the number of columns in the DataFrame + if df.shape[1] < 4: # If the number of columns is less than 4 + print(f"Skipping sheet '{sheet_name}' because it has less than 4 columns.") + continue # Skip to the next sheet + # Extract unique values from column 3 (Python uses zero-based indexing, so the third column is indexed as 2) + unique_values.update(df.iloc[:, 2].unique()) + +# Create a DataFrame with unique values and save it to 'categories.xlsx' +unique_df = pd.DataFrame({'Unique Values': list(unique_values)}) +unique_df.to_excel(f'{output_directory}/categories_income.xlsx', index=False) + + + + + + + + +# finding unique strings in SPENDINGS and categorise it +# Path to the directory containing the file +output_directory2 = '/home/eikov/fin/analysis/' +# Specify the filename +filename2 = 'merged_spending_data.xlsx' +# Construct the full path +file_path2 = f'{output_directory2}/{filename2}' + +# Initialize an empty set to store unique values +unique_values2 = set() + +# Read the Excel file +xls = pd.ExcelFile(file_path2) + +# Iterate over each sheet in the Excel file +for sheet_name in xls.sheet_names: + # Read the sheet into a DataFrame + df = pd.read_excel(xls, sheet_name) + # Check the number of columns in the DataFrame + if df.shape[1] < 4: # If the number of columns is less than 4 + print(f"Skipping sheet '{sheet_name}' because it has less than 4 columns.") + continue # Skip to the next sheet + # Extract unique values from column 3 (Python uses zero-based indexing, so the third column is indexed as 2) + unique_values2.update(df.iloc[:, 2].unique()) + +# Create a DataFrame with unique values and save it to 'categories.xlsx' +unique_df = pd.DataFrame({'Unique Values': list(unique_values2)}) +unique_df.to_excel(f'{output_directory}/categories_spendings.xlsx', index=False) + + + + + + + +""" +#turiu svarius spendings ir income israsus: merged_spending_data.xlsx ir merged_income_data.xlsx +taipogi unikaliu moketoju/gaveju sarasus is pajamu ir islaidu. rankiniu budu priskiriu kiekvienam gavejui/moketojui +kategorijas ir issaugau kaip cat-income.xlsx ir cat-spendings.xlsx. +priedo, manually sukuriau Analysis_income.xlsx ir Analysis_spending.xlsx, kad script'as neoverwritintu mano kategoriju ir analiziu darbo. +belieka prijungti sheets, kad viskas susije su income butu vienam xlsx faile, ir viskas susije su spendings butu antram xlsx faile. +""" +#INCOME +def merge_additional_sheets(base_file, additional_files, output_file): + # Create the output directory if it doesn't exist + output_directory_analysis = '/home/eikov/fin/analysis' + os.makedirs(output_directory_analysis, exist_ok=True) + + # Create a Pandas ExcelWriter object + with pd.ExcelWriter(f'{output_directory_analysis}/{output_file}', engine='xlsxwriter') as writer: + # Write the base file to the output file + base_df = pd.read_excel(f'{output_directory_analysis}/{base_file}', sheet_name=None) + for sheet_name, df in base_df.items(): + df.to_excel(writer, sheet_name=sheet_name, index=False) + + # Write additional files to the output file as new sheets + for file in additional_files: + additional_df = pd.read_excel(f'{output_directory_analysis}/{file}', sheet_name=None) + for sheet_name, df in additional_df.items(): + df.to_excel(writer, sheet_name=f'{sheet_name}', index=False) + +# List of additional files to merge as new sheets +additional_files = ['cat-income.xlsx', 'Analysis_income.xlsx'] + +# Merge additional sheets with merged_income_data.xlsx +merge_additional_sheets('merged_income_data.xlsx', additional_files, 'Galutinis_Income.xlsx') + + +#SPENDINGS +def merge_additional_sheets(base_file, additional_files, output_file): + # Create the output directory if it doesn't exist + output_directory_analysis_s = '/home/eikov/fin/analysis' + os.makedirs(output_directory_analysis_s, exist_ok=True) + + # Create a Pandas ExcelWriter object + with pd.ExcelWriter(f'{output_directory_analysis_s}/{output_file}', engine='xlsxwriter') as writer: + # Write the base file to the output file + base_df = pd.read_excel(f'{output_directory_analysis_s}/{base_file}', sheet_name=None) + for sheet_name, df in base_df.items(): + df.to_excel(writer, sheet_name=sheet_name, index=False) + + # Write additional files to the output file as new sheets + for file in additional_files: + additional_df = pd.read_excel(f'{output_directory_analysis_s}/{file}', sheet_name=None) + for sheet_name, df in additional_df.items(): + df.to_excel(writer, sheet_name=f'{sheet_name}', index=False) + +# List of additional files to merge as new sheets +additional_files = ['cat-spendings.xlsx', 'Analysis_spending.xlsx'] + +# Merge additional sheets with merged_income_data.xlsx +merge_additional_sheets('merged_spending_data.xlsx', additional_files, 'Galutinis_spendings.xlsx') + diff --git a/src/personal_finance/v2.2.py b/src/personal_finance/v2.2.py new file mode 100755 index 0000000..3295fd6 --- /dev/null +++ b/src/personal_finance/v2.2.py @@ -0,0 +1,434 @@ +import pandas as pd +import os +from io import StringIO +import json +import openpyxl + +#more advanced than scrape.py but not fully working yet: +# =sum() shows 0 +# galutinis spendings looks, but income does not merge Categories and Analysis_Income into galutinis_income.xlsx + + + +""" +This code is designed to process bank statement files and organize the data into separate DataFrames for income and spending transactions. It performs the following tasks: + +1. Reads bank statement files from a specified directory. +2. Extracts relevant information from each file, such as transaction date, amount, payer/payee name, and purpose. +3. Applies data cleaning and preprocessing steps, including removing unnecessary columns and extracting the first two words from payer/payee names and payment purposes. +4. Organizes the data into separate DataFrames for income and spending transactions, grouped by month. +5. Saves the DataFrames as JSON and Excel files in separate directories for income and spending. +6. Merges separate monthly Excel files into single "merged_income_data.xlsx" and "merged_spending_data.xlsx" files. +7. Extracts unique payer/payee names from the merged files and saves them as "unique-strings-income.xlsx" and "unique-strings-spendings.xlsx". +8. Allows manual categorization of payer/payee names by creating "cat-income.xlsx" and "cat-spendings.xlsx" files. +9. Merges the categorized data and analysis sheets into final "Galutinis_Income.xlsx" and "Galutinis_spendings.xlsx" files. + +The code is designed to be modular and extensible, allowing for easy integration of additional data processing or analysis steps as needed. +""" + +""" +#next: in 8: create DF for each found excel spending(-) file(ignore the last row with total sum) and join into one DF. +now iterate each unique value and sum all "SUMA" values. if that's not possible, convert DF to set and then sum key values. +""" + + +# Define the directory containing the CSV files +directory = '/home/eikov/fin/OG-CSV' + +# Define the pattern for file names +file_pattern = '{}{}.csv' # {} will be replaced by the file number and +/- for the file type + +# Initialize empty dictionaries to store dataframes for each month +spending_dfs = {} +income_dfs = {} + +# Define the filters +filter1 = "KOVALIŪNAS EINARAS" +filter2 = "Einaras Kovaliūnas" # Add your second filter value here + + +# Function to extract first 2 words from a string +def extract_first_2_words(text): + if pd.isna(text): # Check if the value is NaN + return "" # Return an empty string if NaN + words = text.split()[:2] + return ' '.join(words) + + + +# Iterate over the file numbers from 1 to 12 +for file_num in range(1, 13): + month = str(file_num) + spending_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + income_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + for file_type in ['+', '-']: + filename = file_pattern.format(file_num, file_type) + filepath = os.path.join(directory, filename) + if os.path.exists(filepath): + print(f"Processing file: {filename}") + # Initialize an empty list to store the valid lines of the CSV file + valid_lines = [] + skipped_rows = [] + + # Read the CSV file line by line + with open(filepath, 'r', encoding='utf-8') as file: + for i, line in enumerate(file): + if i < 2: # Skip the first two lines (first row and header row) + continue + try: + # Attempt to parse the line as CSV + pd.read_csv(StringIO(line), header=None, sep=';') # Use header=None and specify separator as ';' + valid_lines.append(line) # If successful, add the line to the list of valid lines + except pd.errors.ParserError: + print(f"Skipped line {i+1} in file {filename} due to ParserError: {line.strip()}") + skipped_rows.append((i+1, line.strip())) # Store the line number and the content of the skipped row + + # Read the valid lines into a DataFrame, skipping the header line + bank_data = None + try: + bank_data = pd.read_csv(StringIO(''.join(valid_lines)), header=None, sep=';') + except pd.errors.ParserError: + print(f"Encountered an error while parsing file {filename}. Skipping problematic rows.") + if not valid_lines: + print("No valid data read due to errors.") + continue + + # If data was successfully read, extract specific columns and apply filter + if bank_data is not None: + bank_data = bank_data[[1, 3, 4, 9]] # Extract only desired columns + + # Replace empty values with NaN + bank_data.replace('-', pd.NA, inplace=True) + + # Rename columns + bank_data.columns = ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"] + + # Apply filters + bank_data = bank_data[(bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter1) & + (bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter2)] + + # Filter out rows that are the same as column headers or contain all empty values + bank_data = bank_data[~bank_data.apply(lambda row: all(row == ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]), axis=1)] + + # Extract first 4 words from specified columns + bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] = bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"].apply(extract_first_2_words) + bank_data["MOKĖJIMO PASKIRTIS"] = bank_data["MOKĖJIMO PASKIRTIS"].apply(extract_first_2_words) + + # Replace commas with periods in the "SUMA" column + bank_data["SUMA"] = bank_data["SUMA"].str.replace(',', '.') + + # Convert the values in the "SUMA" column to numeric data type + bank_data["SUMA"] = pd.to_numeric(bank_data["SUMA"], errors='coerce') + # Now the "SUMA" column contains numeric values + + + # Append the DataFrame to the appropriate dictionary based on file type + if file_type == '+': + income_dfs[month] = bank_data + elif file_type == '-': + spending_dfs[month] = bank_data + else: + print(f"No valid data read from file {filename}.") + + # Display the DataFrame + if file_type == '+': + print(f"\nIncome DataFrame for month {month}:") + print(income_dfs[month]) + elif file_type == '-': + print(f"\nSpending DataFrame for month {month}:") + print(spending_dfs[month]) + + # Specify the folder path + output_folder_income = "/home/eikov/fin/income" + output_folder_spending = "/home/eikov/fin/spendings" + + # Create the folder if it doesn't exist + os.makedirs(output_folder_income, exist_ok=True) + os.makedirs(output_folder_spending, exist_ok=True) + + # Save DataFrames to JSON files + income_json_filename = os.path.join( output_folder_income, f"income_month_{month}.json") + spending_json_filename = os.path.join(output_folder_spending, f"spending_month_{month}.json") + + # Save DataFrames to Excel files + income_excel_filename = os.path.join( output_folder_income, f"income_month_{month}.xlsx") + spending_excel_filename = os.path.join(output_folder_spending, f"spending_month_{month}.xlsx") + + # Drop rows with NaN values + income_dfs[month].dropna(inplace=True) + spending_dfs[month].dropna(inplace=True) + + # Convert DataFrames to JSON strings + income_json_str = income_dfs[month].to_json(orient='records', lines=True, default_handler=str) + spending_json_str = spending_dfs[month].to_json(orient='records', lines=True, default_handler=str) + + # Save DataFrames to Excel files + income_dfs[month].to_excel(income_excel_filename, index=False) + spending_dfs[month].to_excel(spending_excel_filename, index=False) + + print(f"DataFrames saved to JSON files: {income_json_filename}, {spending_json_filename}") + print(f"DataFrames saved to Excel files: {income_excel_filename}, {spending_excel_filename}") + + # Display skipped rows + if skipped_rows: + print(f"\nSkipped rows in file {filename}:") + for row_num, row_content in skipped_rows: + print(f"Row {row_num}: {row_content}") + else: + print(f"File {filename} not found. Skipping...") + +print("All files processed.") + + + + + + + + +#Merge spending seperate xlsx files to one +directory = '/home/eikov/fin/spendings' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + + # Add total sum formula(B2:B100) at the end of the last row + last_row = merged_sheet.max_row + formula_str = f'=SUM(B2:B100)' + merged_sheet[f'A{last_row + 1}'] = formula_str + + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_spending_data.xlsx") +merged_workbook.save(output_filename) + + +#Merge income seperate xlsx files to one +directory = '/home/eikov/fin/income' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + + # Add total sum formula(B2:B100) at the end of the last row + last_row = merged_sheet.max_row + formula_str = f'=SUM(B2:B100)' + merged_sheet[f'A{last_row + 1}'] = formula_str + + + # Save the merged workbook to the output directory + output_filename = os.path.join(output_directory, "merged_income_data.xlsx") + merged_workbook.save(output_filename) + + + + + + + + + + + +# finding unique strings in INCOME and categorise it +# Path to the directory containing the file +output_directory = '/home/eikov/fin/analysis/' +# Specify the filename +filename = 'merged_income_data.xlsx' +# Construct the full path +file_path = f'{output_directory}/{filename}' + +# Initialize an empty set to store unique values +unique_values = set() + +# Read the Excel file +xls = pd.ExcelFile(file_path) + +# Iterate over each sheet in the Excel file +for sheet_name in xls.sheet_names: + # Read the sheet into a DataFrame + df = pd.read_excel(xls, sheet_name) + # Check the number of columns in the DataFrame + if df.shape[1] < 4: # If the number of columns is less than 4 + print(f"Skipping sheet '{sheet_name}' because it has less than 4 columns.") + continue # Skip to the next sheet + # Extract unique values from column 3 (Python uses zero-based indexing, so the third column is indexed as 2) + unique_values.update(df.iloc[:, 2].unique()) + +# Create a DataFrame with unique values and save it to 'unique-strings-income.xlsx' +unique_df = pd.DataFrame({'Unique Values': list(unique_values)}) +unique_df.to_excel(f'{output_directory}/unique-strings-income.xlsx', index=False) + + + + + + + + +# finding unique strings in SPENDINGS and categorise it +# Path to the directory containing the file +output_directory2 = '/home/eikov/fin/analysis/' +# Specify the filename +filename2 = 'merged_spending_data.xlsx' +# Construct the full path +file_path2 = f'{output_directory2}/{filename2}' + +# Initialize an empty set to store unique values +unique_values2 = set() + +# Read the Excel file +xls = pd.ExcelFile(file_path2) + +# Iterate over each sheet in the Excel file +for sheet_name in xls.sheet_names: + # Read the sheet into a DataFrame + df = pd.read_excel(xls, sheet_name) + # Check the number of columns in the DataFrame + if df.shape[1] < 4: # If the number of columns is less than 4 + print(f"Skipping sheet '{sheet_name}' because it has less than 4 columns.") + continue # Skip to the next sheet + # Extract unique values from column 3 (Python uses zero-based indexing, so the third column is indexed as 2) + unique_values2.update(df.iloc[:, 2].unique()) + +# Create a DataFrame with unique values and save it to 'unique-strings-spendings.' +unique_df = pd.DataFrame({'Unique Values': list(unique_values2)}) +unique_df.to_excel(f'{output_directory}/unique-strings-spendings.xlsx', index=False) + + + + + +""" +1) AUTO: turiu svarius spendings ir income israsus: merged_spending_data.xlsx ir merged_income_data.xlsx +2) MANUAL: turiu unikaliu moketoju/gaveju sarasus is pajamu ir islaidu[unique-strings-.xlsx]. rankiniu budu priskiriu kiekvienam gavejui/moketojui +kategorijas ir issaugau kaip cat-income.xlsx ir cat-spendings.xlsx. +3) MANUAL: manually sukuriau Analysis_income.xlsx ir Analysis_spending.xlsx, kad script'as neoverwritintu mano kategoriju ir analiziu darbo. +4) AUTO: belieka prijungti sheets, kad viskas susije su income butu vienam xlsx faile, ir viskas susije su spendings butu antram xlsx faile. + + # Save the merged workbook to the output directory + output_filename = os.path.join(output_directory, "merged_income_data.xlsx") + merged_workbook.save(output_filename) +""" + +#INCOME +def merge_additional_sheets(base_file, additional_files, output_file): + # Create the output directory if it doesn't exist + output_directory_analysis = '/home/eikov/fin/analysis' + os.makedirs(output_directory_analysis, exist_ok=True) + + # Create a Pandas ExcelWriter object + with pd.ExcelWriter(f'{output_directory_analysis}/{output_file}', engine='xlsxwriter') as writer: + # Write the base file to the output file + base_df = pd.read_excel(f'{output_directory_analysis}/{base_file}', sheet_name=None) + for sheet_name, df in base_df.items(): + df.to_excel(writer, sheet_name=sheet_name, index=False) + # Add total sum formula to the end of the last row + last_row = df.shape[0] + 1 + formula_str = f'=SUM(B2:B{last_row})' + writer.sheets[sheet_name].write(f'A{last_row + 1}', formula_str) + + # Write additional files to the output file as new sheets + for file in additional_files: + additional_df = pd.read_excel(f'{output_directory_analysis}/{file}', sheet_name=None) + for sheet_name, df in additional_df.items(): + df.to_excel(writer, sheet_name=f'{sheet_name}', index=False) + # Add total sum formula to the end of the last row + if file not in additional_files: + last_row = df.shape[0] + 1 + formula_str = f'=SUM(B2:B{last_row})' + writer.sheets[sheet_name].write(f'A{last_row + 1}', formula_str) + +# List of additional files to merge as new sheets. +additional_files = ['cat-income.xlsx', 'Analysis_income.xlsx'] + +# Merge additional sheets with merged_income_data.xlsx +merge_additional_sheets('merged_income_data.xlsx', additional_files, 'Galutinis_Income.xlsx') + + +#SPENDINGS +def merge_additional_sheets(base_file, additional_files, output_file): + # Create the output directory if it doesn't exist + output_directory_analysis = '/home/eikov/fin/analysis' + os.makedirs(output_directory_analysis, exist_ok=True) + + # Create a Pandas ExcelWriter object + with pd.ExcelWriter(f'{output_directory_analysis}/{output_file}', engine='xlsxwriter') as writer: + # Write the base file to the output file + base_df = pd.read_excel(f'{output_directory_analysis}/{base_file}', sheet_name=None) + for sheet_name, df in base_df.items(): + df.to_excel(writer, sheet_name=sheet_name, index=False) + # Add total sum formula to the end of the last row + last_row = df.shape[0] + 1 + formula_str = f'=SUM(B2:B{last_row})' + writer.sheets[sheet_name].write(f'A{last_row + 1}', formula_str) + + # Write additional files to the output file as new sheets + for file in additional_files: + additional_df = pd.read_excel(f'{output_directory_analysis}/{file}', sheet_name=None) + for sheet_name, df in additional_df.items(): + df.to_excel(writer, sheet_name=f'{sheet_name}', index=False) + # Add total sum formula to the end of the last row + if file not in additional_files: + last_row = df.shape[0] + 1 + formula_str = f'=SUM(B2:B{last_row})' + writer.sheets[sheet_name].write(f'A{last_row + 1}', formula_str) + +# List of additional files to merge as new sheets +additional_files = ['cat-spendings.xlsx', 'Analysis_spendings.xlsx'] + +# Merge additional sheets with merged_income_data.xlsx +merge_additional_sheets('merged_spending_data.xlsx', additional_files, 'Galutinis_spendings.xlsx') diff --git a/src/personal_finance/v2.3.py b/src/personal_finance/v2.3.py new file mode 100755 index 0000000..e74be07 --- /dev/null +++ b/src/personal_finance/v2.3.py @@ -0,0 +1,474 @@ +import pandas as pd +import os +from io import StringIO +import json +import openpyxl + +#more advanced than scrape.py but not fully working yet: +# =sum() shows 0 +# galutinis spendings looks, but income does not merge Categories and Analysis_Income into galutinis_income.xlsx + +#TBD: +# a script scraped all entries and then gave me unique string values in .xlsx format. +# 1) I want to see the sum each unique string . What's the best approach to do this, in pandas before writing to .xlsx or somehow edit it after saving? +# 2) Then as a second [B] column I assigned each unique string value a sub-category and [C] column as Category. to do analysis on different Sheet named [Analysis] + + +""" +This code is designed to process bank statement files and organize the data into separate DataFrames for income and spending transactions. It performs the following tasks: + +1. Reads bank statement files from a specified directory. +2. Extracts relevant information from each file, such as transaction date, amount, payer/payee name, and purpose. +3. Applies data cleaning and preprocessing steps, including removing unnecessary columns and extracting the first two words from payer/payee names and payment purposes. +4. Organizes the data into separate DataFrames for income and spending transactions, grouped by month. +5. Saves the DataFrames as JSON and Excel files in separate directories for income and spending. +6. Merges separate monthly Excel files into single "merged_income_data.xlsx" and "merged_spending_data.xlsx" files. +7. Extracts unique payer/payee names from the merged files and saves them as "unique-strings-income.xlsx" and "unique-strings-spendings.xlsx". +8. Allows manual categorization of payer/payee names by creating "cat-income.xlsx" and "cat-spendings.xlsx" files. +9. Merges the categorized data and analysis sheets into final "Galutinis_Income.xlsx" and "Galutinis_spendings.xlsx" files. + +The code is designed to be modular and extensible, allowing for easy integration of additional data processing or analysis steps as needed. +""" + +""" +#next: in 8: create DF for each found excel spending(-) file(ignore the last row with total sum) and join into one DF. +now iterate each unique value and sum all "SUMA" values. if that's not possible, convert DF to set and then sum key values. +""" + + +# Define the directory containing the CSV files +directory = '/home/eikov/fin/OG-CSV' + +# Define the pattern for file names +file_pattern = '{}{}.csv' # {} will be replaced by the file number and +/- for the file type + +# Initialize empty dictionaries to store dataframes for each month +spending_dfs = {} +income_dfs = {} + +# Define the filters +filter1 = "KOVALIŪNAS EINARAS" +filter2 = "Einaras Kovaliūnas" # Add your second filter value here + + +# Function to extract first 2 words from a string +def extract_first_2_words(text): + if pd.isna(text): # Check if the value is NaN + return "" # Return an empty string if NaN + words = text.split()[:2] + return ' '.join(words) + + + +# Iterate over the file numbers from 1 to 12 +for file_num in range(1, 13): + month = str(file_num) + spending_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + income_dfs[month] = pd.DataFrame(columns=["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]) + for file_type in ['+', '-']: + filename = file_pattern.format(file_num, file_type) + filepath = os.path.join(directory, filename) + if os.path.exists(filepath): + print(f"Processing file: {filename}") + # Initialize an empty list to store the valid lines of the CSV file + valid_lines = [] + skipped_rows = [] + + # Read the CSV file line by line + with open(filepath, 'r', encoding='utf-8') as file: + for i, line in enumerate(file): + if i < 2: # Skip the first two lines (first row and header row) + continue + try: + # Attempt to parse the line as CSV + pd.read_csv(StringIO(line), header=None, sep=';') # Use header=None and specify separator as ';' + valid_lines.append(line) # If successful, add the line to the list of valid lines + except pd.errors.ParserError: + print(f"Skipped line {i+1} in file {filename} due to ParserError: {line.strip()}") + skipped_rows.append((i+1, line.strip())) # Store the line number and the content of the skipped row + + # Read the valid lines into a DataFrame, skipping the header line + bank_data = None + try: + bank_data = pd.read_csv(StringIO(''.join(valid_lines)), header=None, sep=';') + except pd.errors.ParserError: + print(f"Encountered an error while parsing file {filename}. Skipping problematic rows.") + if not valid_lines: + print("No valid data read due to errors.") + continue + + # If data was successfully read, extract specific columns and apply filter + if bank_data is not None: + bank_data = bank_data[[1, 3, 4, 9]] # Extract only desired columns + + # Replace empty values with NaN + bank_data.replace('-', pd.NA, inplace=True) + + # Rename columns + bank_data.columns = ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"] + + # Apply filters + bank_data = bank_data[(bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter1) & + (bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] != filter2)] + + # Filter out rows that are the same as column headers or contain all empty values + bank_data = bank_data[~bank_data.apply(lambda row: all(row == ["DATA", "SUMA", "MOKĖTOJO ARBA GAVĖJO PAVADINIMAS", "MOKĖJIMO PASKIRTIS"]), axis=1)] + + # Extract first 4 words from specified columns + bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"] = bank_data["MOKĖTOJO ARBA GAVĖJO PAVADINIMAS"].apply(extract_first_2_words) + bank_data["MOKĖJIMO PASKIRTIS"] = bank_data["MOKĖJIMO PASKIRTIS"].apply(extract_first_2_words) + + # Replace commas with periods in the "SUMA" column + bank_data["SUMA"] = bank_data["SUMA"].str.replace(',', '.') + + # Convert the values in the "SUMA" column to numeric data type + bank_data["SUMA"] = pd.to_numeric(bank_data["SUMA"], errors='coerce') + # Now the "SUMA" column contains numeric values + + + # Append the DataFrame to the appropriate dictionary based on file type + if file_type == '+': + income_dfs[month] = bank_data + elif file_type == '-': + spending_dfs[month] = bank_data + else: + print(f"No valid data read from file {filename}.") + + # Display the DataFrame + if file_type == '+': + print(f"\nIncome DataFrame for month {month}:") + print(income_dfs[month]) + elif file_type == '-': + print(f"\nSpending DataFrame for month {month}:") + print(spending_dfs[month]) + + # Specify the folder path + output_folder_income = "/home/eikov/fin/income" + output_folder_spending = "/home/eikov/fin/spendings" + + # Create the folder if it doesn't exist + os.makedirs(output_folder_income, exist_ok=True) + os.makedirs(output_folder_spending, exist_ok=True) + + # Save DataFrames to JSON files + income_json_filename = os.path.join( output_folder_income, f"income_month_{month}.json") + spending_json_filename = os.path.join(output_folder_spending, f"spending_month_{month}.json") + + # Save DataFrames to Excel files + income_excel_filename = os.path.join( output_folder_income, f"income_month_{month}.xlsx") + spending_excel_filename = os.path.join(output_folder_spending, f"spending_month_{month}.xlsx") + + # Drop rows with NaN values + income_dfs[month].dropna(inplace=True) + spending_dfs[month].dropna(inplace=True) + + # Convert DataFrames to JSON strings + income_json_str = income_dfs[month].to_json(orient='records', lines=True, default_handler=str) + spending_json_str = spending_dfs[month].to_json(orient='records', lines=True, default_handler=str) + + # Save DataFrames to Excel files + income_dfs[month].to_excel(income_excel_filename, index=False) + spending_dfs[month].to_excel(spending_excel_filename, index=False) + + print(f"DataFrames saved to JSON files: {income_json_filename}, {spending_json_filename}") + print(f"DataFrames saved to Excel files: {income_excel_filename}, {spending_excel_filename}") + + # Display skipped rows + if skipped_rows: + print(f"\nSkipped rows in file {filename}:") + for row_num, row_content in skipped_rows: + print(f"Row {row_num}: {row_content}") + else: + print(f"File {filename} not found. Skipping...") + +print("All files processed.") + + + + + + + + +#Merge spending seperate xlsx files to one +directory = '/home/eikov/fin/spendings' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + sheet = workbook.active + + if i == 0: + merged_sheet = merged_workbook.active + else: + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + for row in sheet.iter_rows(values_only=True): + if not any(row): # skip empty rows + continue + merged_sheet.append(row) + + # Add total formula + data_start_row = 2 + data_end_row = merged_sheet.max_row + formula_str = f'=SUM(B{data_start_row}:B{data_end_row})' + merged_sheet[f'A{data_end_row + 1}'] = 'TOTAL' + merged_sheet[f'B{data_end_row + 1}'] = formula_str + + +# Save the merged workbook to the output directory +output_filename = os.path.join(output_directory, "merged_spending_data.xlsx") +merged_workbook.save(output_filename) + + +#Merge income seperate xlsx files to one +directory = '/home/eikov/fin/income' +output_directory = '/home/eikov/fin/analysis' + +# List all files in the directory +files = os.listdir(directory) + +# Filter out only the Excel files (files ending with .xlsx) +excel_files = [file for file in files if file.endswith('.xlsx')] + +# Create a new workbook +merged_workbook = openpyxl.Workbook() + +# Loop through each file and add it as a new sheet to the workbook +for i, file_name in enumerate(excel_files): + # Load the workbook from file + workbook = openpyxl.load_workbook(os.path.join(directory, file_name)) + + # Get the active sheet from the loaded workbook + sheet = workbook.active + + # Copy the active sheet to the merged workbook + if i == 0: + # For the first sheet, use the default sheet name 'Sheet' + merged_sheet = merged_workbook.active + else: + # For subsequent sheets, create a new sheet with the file name as the sheet name + merged_sheet = merged_workbook.create_sheet(title=f"Sheet{i+1}") + + # Copy data from the original sheet to the merged sheet + for row in sheet.iter_rows(values_only=True): + merged_sheet.append(row) + + # Add total sum formula(B2:B100) at the end of the last row + last_row = merged_sheet.max_row + formula_str = f'=SUM(B2:B100)' + merged_sheet[f'A{last_row + 1}'] = formula_str + + + # Save the merged workbook to the output directory + output_filename = os.path.join(output_directory, "merged_income_data.xlsx") + merged_workbook.save(output_filename) + + + + + + + + + + + +# finding unique strings in SPENDINGS and categorise it +# Path to the directory containing the file +output_directory2 = '/home/eikov/fin/analysis' +filename2 = 'merged_income_data.xlsx' +file_path2 = f'{output_directory2}/{filename2}' + +# Read Excel file +xls = pd.ExcelFile(file_path2) + +# Create empty DataFrame to collect values +all_data = pd.DataFrame(columns=['SUMA', 'MOKĖTOJO ARBA GAVĖJO PAVADINIMAS']) + +# Iterate through each sheet +for sheet_name in xls.sheet_names: + df = pd.read_excel(xls, sheet_name) + + if df.shape[1] < 4: + print(f"Skipping sheet '{sheet_name}' because it has less than 4 columns.") + continue + + # Extract 'SUMA' and 'STRING' columns by index — adjust if needed + amounts = df.iloc[:, 1] # this is column B, the 'SUMA' + labels = df.iloc[:, 2] # this is column C, the 'MOKĖTOJO ARBA GAVĖJO PAVADINIMAS' + + + # Combine into one DataFrame + temp_df = pd.DataFrame({'SUMA': amounts, 'MOKĖTOJO ARBA GAVĖJO PAVADINIMAS': labels}) + + # Drop rows where either SUMA or STRING is missing + temp_df.dropna(subset=['SUMA', 'MOKĖTOJO ARBA GAVĖJO PAVADINIMAS'], inplace=True) + + # 🛠 Clean and convert SUMA properly + temp_df['SUMA'] = ( + temp_df['SUMA'] + .astype(str) + .str.replace(r'[^\d,.-]', '', regex=True) + .str.replace(',', '.', regex=False) + .astype(float) + ) + + + all_data = pd.concat([all_data, temp_df], ignore_index=True) + +# Group and sum +grouped = all_data.groupby('MOKĖTOJO ARBA GAVĖJO PAVADINIMAS', as_index=False)['SUMA'].sum() + +# Save results +grouped.to_excel(f'{output_directory2}/summed-strings-income.xlsx', index=False) + + + + + + + + +# finding unique strings in SPENDINGS and categorise it +# Path to the directory containing the file +output_directory2 = '/home/eikov/fin/analysis' +filename2 = 'merged_spending_data.xlsx' +file_path2 = f'{output_directory2}/{filename2}' + +# Read Excel file +xls = pd.ExcelFile(file_path2) + +# Create empty DataFrame to collect values +all_data = pd.DataFrame(columns=['SUMA', 'MOKĖTOJO ARBA GAVĖJO PAVADINIMAS']) + +# Iterate through each sheet +for sheet_name in xls.sheet_names: + df = pd.read_excel(xls, sheet_name) + + if df.shape[1] < 4: + print(f"Skipping sheet '{sheet_name}' because it has less than 4 columns.") + continue + + # Extract 'SUMA' and 'STRING' columns by index — adjust if needed + amounts = df.iloc[:, 1] # this is column B, the 'SUMA' + labels = df.iloc[:, 2] # this is column C, the 'MOKĖTOJO ARBA GAVĖJO PAVADINIMAS' + + + # Combine into one DataFrame + temp_df = pd.DataFrame({'SUMA': amounts, 'MOKĖTOJO ARBA GAVĖJO PAVADINIMAS': labels}) + + # Drop rows where either SUMA or STRING is missing + temp_df.dropna(subset=['SUMA', 'MOKĖTOJO ARBA GAVĖJO PAVADINIMAS'], inplace=True) + + # 🛠 Clean and convert SUMA properly + temp_df['SUMA'] = ( + temp_df['SUMA'] + .astype(str) + .str.replace(r'[^\d,.-]', '', regex=True) + .str.replace(',', '.', regex=False) + .astype(float) + ) + + all_data = pd.concat([all_data, temp_df], ignore_index=True) + +# Group and sum +grouped = all_data.groupby('MOKĖTOJO ARBA GAVĖJO PAVADINIMAS', as_index=False)['SUMA'].sum() + +# Save results +grouped.to_excel(f'{output_directory2}/summed-strings-spendings.xlsx', index=False) + + + + + +""" +1) AUTO: turiu svarius spendings ir income israsus: merged_spending_data.xlsx ir merged_income_data.xlsx +2) MANUAL: turiu unikaliu moketoju/gaveju sarasus is pajamu ir islaidu[summed-strings-income/spendings.xlsx]. rankiniu budu priskiriu kiekvienam gavejui/moketojui +kategorijas ir issaugau kaip cat-income.xlsx ir cat-spendings.xlsx. +3) MANUAL: manually sukuriau Analysis_income.xlsx ir Analysis_spending.xlsx, kad script'as neoverwritintu mano kategoriju ir analiziu darbo. +4) AUTO: belieka prijungti sheets, kad viskas susije su income butu vienam xlsx faile, ir viskas susije su spendings butu antram xlsx faile. + + # Save the merged workbook to the output directory + output_filename = os.path.join(output_directory, "merged_income_data.xlsx") + merged_workbook.save(output_filename) +""" + +#INCOME +def merge_additional_sheets(base_file, additional_files, output_file): + # Create the output directory if it doesn't exist + output_directory_analysis = '/home/eikov/fin/analysis' + os.makedirs(output_directory_analysis, exist_ok=True) + + # Create a Pandas ExcelWriter object + with pd.ExcelWriter(f'{output_directory_analysis}/{output_file}', engine='xlsxwriter') as writer: + # Write the base file to the output file + base_df = pd.read_excel(f'{output_directory_analysis}/{base_file}', sheet_name=None) + for sheet_name, df in base_df.items(): + df.to_excel(writer, sheet_name=sheet_name, index=False) + # Add total sum formula to the end of the last row + last_row = df.shape[0] + 1 + formula_str = f'=SUM(B2:B{last_row})' + writer.sheets[sheet_name].write(f'A{last_row + 1}', formula_str) + + # Write additional files to the output file as new sheets + for file in additional_files: + additional_df = pd.read_excel(f'{output_directory_analysis}/{file}', sheet_name=None) + for sheet_name, df in additional_df.items(): + df.to_excel(writer, sheet_name=f'{sheet_name}', index=False) + # Add total sum formula to the end of the last row + if file not in additional_files: + last_row = df.shape[0] + 1 + formula_str = f'=SUM(B2:B{last_row})' + writer.sheets[sheet_name].write(f'A{last_row + 1}', formula_str) + +# List of additional files to merge as new sheets. +additional_files = ['cat-income.xlsx', 'Analysis_income.xlsx'] + +# Merge additional sheets with merged_income_data.xlsx +merge_additional_sheets('merged_income_data.xlsx', additional_files, 'Galutinis_Income.xlsx') + + +#SPENDINGS +def merge_additional_sheets(base_file, additional_files, output_file): + # Create the output directory if it doesn't exist + output_directory_analysis = '/home/eikov/fin/analysis' + os.makedirs(output_directory_analysis, exist_ok=True) + + # Create a Pandas ExcelWriter object + with pd.ExcelWriter(f'{output_directory_analysis}/{output_file}', engine='xlsxwriter') as writer: + # Write the base file to the output file + base_df = pd.read_excel(f'{output_directory_analysis}/{base_file}', sheet_name=None) + for sheet_name, df in base_df.items(): + df.to_excel(writer, sheet_name=sheet_name, index=False) + # Add total sum formula to the end of the last row + last_row = df.shape[0] + 1 + formula_str = f'=SUM(B2:B{last_row})' + writer.sheets[sheet_name].write(f'A{last_row + 1}', formula_str) + + # Write additional files to the output file as new sheets + for file in additional_files: + additional_df = pd.read_excel(f'{output_directory_analysis}/{file}', sheet_name=None) + for sheet_name, df in additional_df.items(): + df.to_excel(writer, sheet_name=f'{sheet_name}', index=False) + # Add total sum formula to the end of the last row + if file not in additional_files: + last_row = df.shape[0] + 1 + formula_str = f'=SUM(B2:B{last_row})' + writer.sheets[sheet_name].write(f'A{last_row + 1}', formula_str) + +# List of additional files to merge as new sheets +additional_files = ['cat-spendings.xlsx', 'Analysis_spendings.xlsx'] + +# Merge additional sheets with merged_income_data.xlsx +merge_additional_sheets('merged_spending_data.xlsx', additional_files, 'Galutinis_spendings.xlsx') diff --git a/src/personal_notes/clickup_extract/plan.txt b/src/personal_notes/clickup_extract/plan.txt new file mode 100755 index 0000000..14aac82 --- /dev/null +++ b/src/personal_notes/clickup_extract/plan.txt @@ -0,0 +1,11 @@ +extractinu data is clickup, palieku tik: +1) project management category ~30lines +2) data programming ~40 lines +3) climate and life ~15lines +4) to-do + +should be fine for free version. if not, leave only 1) and 2) +3) and 4) to Joplin + + + diff --git a/src/personal_notes/clickup_extract/tempy.py b/src/personal_notes/clickup_extract/tempy.py new file mode 100755 index 0000000..c1bc655 --- /dev/null +++ b/src/personal_notes/clickup_extract/tempy.py @@ -0,0 +1,104 @@ +import os +import pandas as pd +import re + +# Specify the CSV file path +csv_file = 'clickup10-25.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Strip whitespace from column names immediately after loading +df.columns = df.columns.str.strip() + +# Columns to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop specified columns +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Define output directory +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +# Define a maximum length for folder and file names (adjust as needed) +MAX_NAME_LENGTH = 100 + +# Utility function to sanitize names +def sanitize_name(name): + # Remove invalid characters and truncate + sanitized = re.sub(r'[<>:"/\\|?*]', '', str(name)) + return sanitized[:MAX_NAME_LENGTH] # Truncate if necessary + +# Process each unique List Name +for value in unique_values: + sanitized_list_name = sanitize_name(value) # Sanitize List Name + list_folder = os.path.join(output_dir, sanitized_list_name) + os.makedirs(list_folder, exist_ok=True) + + # Filter DataFrame based on the current List Name value + filtered_df = df[df['List Name'] == value] + + # Drop columns 'Space Name' and 'List Name' + filtered_df = filtered_df.drop(columns=['Space Name', 'List Name']) + + # Prepare a list to hold sorted rows + sorted_rows = [] + + # Define function to add tasks and their children + def add_task_with_children(task_id): + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task) + + # Find top-level tasks (no parent) and create folders for each + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()] + + for _, task_row in top_level_tasks.iterrows(): + task_id = task_row['Task ID'] + task_name = task_row['Task Name'] + + # Sanitize the task name for folder and file naming + sanitized_task_name = sanitize_name(task_name) # Sanitize Task Name + + # Create folder for the top-level task using the sanitized name + task_folder = os.path.join(list_folder, sanitized_task_name) + os.makedirs(task_folder, exist_ok=True) + + # Collect children of the top-level task + sorted_rows.clear() + add_task_with_children(task_id) + + # Convert to DataFrame + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + + # Name the text file based on the parent task's name + txt_output_file = os.path.join(task_folder, f'{sanitized_task_name}.txt') + + # Check for filename length and truncate if necessary + if len(txt_output_file) > 255: + txt_output_file = os.path.join(task_folder, f'{sanitized_task_name[:255]}.txt') + + txt_columns_to_exclude = ['Task ID', 'Parent ID'] + sorted_df_txt = sorted_df.drop(columns=txt_columns_to_exclude) + sorted_df_txt = sorted_df_txt.replace(['NaN', '[]'], '') + + # Write to the text file + with open(txt_output_file, 'w', encoding='utf-8') as f: + for _, row in sorted_df_txt.iterrows(): + cleaned_row = [f"(({val}))" if pd.notna(val) and col == 'Task Content' else str(val) if pd.notna(val) else '' + for col, val in zip(sorted_df_txt.columns, row.values)] + f.write('\t'.join(cleaned_row) + '\n') + f.write("----------------------------------------------------\n") # Add separator line after each child's content + +print(f'Processed {len(df)} rows and saved them in the "{output_dir}" directory with the specified folder structure.') diff --git a/src/personal_notes/clickup_extract/v1clean.py b/src/personal_notes/clickup_extract/v1clean.py new file mode 100755 index 0000000..4dec753 --- /dev/null +++ b/src/personal_notes/clickup_extract/v1clean.py @@ -0,0 +1,40 @@ +import os +import pandas as pd + +#preparing clickup exported CSV file for cleaning unnecessary columns +# Read the CSV file into a DataFrame +csv_file = 'clickup2.csv' # Replace 'your_file.csv' with the path to your CSV file +df = pd.read_csv(csv_file) + +# Specify the columns you want to delete +columns_to_delete = ['Task ID', 'Attachments', 'Date Created', 'Date Created Text','Due Date', 'Due Date Text','Start Date', 'Start Date Text','Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', 'Time Spent', 'Time Spent Text', + 'Rolled Up Time', 'Rolled Up Time Text', + 'Parent ID', 'Tags', 'Priority', 'Status'] +# Replace with the names of columns you want to delete + +# Drop the specified columns from the DataFrame +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get all the unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() +# Print or use the unique values as needed +print(unique_values) + +# Write the modified DataFrame back to a CSV file +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) +for value in unique_values: + # Sanitize the string to replace problematic characters + sanitized_value = value.replace('/', '_') # Replace '/' with '_' + output_file = os.path.join(output_dir, f'{sanitized_value}.csv') + # Filter the DataFrame based on the current value + filtered_df = df[df['List Name'] == value] + # Drop additional columns from the filtered DataFrame + additional_columns_to_drop = ['Space Name', 'List Name'] # Replace with the names of columns you want to drop + filtered_df = filtered_df.drop(columns=additional_columns_to_drop) + # Write the filtered DataFrame to a CSV file + filtered_df.to_csv(output_file, index=False) diff --git a/src/personal_notes/clickup_extract/v2sorted_messy.py b/src/personal_notes/clickup_extract/v2sorted_messy.py new file mode 100755 index 0000000..ff20c3e --- /dev/null +++ b/src/personal_notes/clickup_extract/v2sorted_messy.py @@ -0,0 +1,74 @@ +import os +import pandas as pd + +# Preparing clickup exported CSV file for cleaning unnecessary columns +csv_file = 'clickup2.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Specify the columns you want to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop the specified columns from the DataFrame +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get all the unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Print or use the unique values as needed +print(unique_values) + +# Write the modified DataFrame back to a CSV and TXT files +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +for value in unique_values: + # Sanitize the string to replace problematic characters + sanitized_value = value.replace('/', '_') # Replace '/' with '_' + csv_output_file = os.path.join(output_dir, f'{sanitized_value}.csv') + txt_output_file = os.path.join(output_dir, f'{sanitized_value}.txt') + + # Filter the DataFrame based on the current value + filtered_df = df[df['List Name'] == value] + + # Drop additional columns from the filtered DataFrame + additional_columns_to_drop = ['Space Name', 'List Name'] # Replace with the names of columns you want to drop + filtered_df = filtered_df.drop(columns=additional_columns_to_drop) + + # Create a list to hold the final sorted rows + sorted_rows = [] + + # Function to add tasks and their children + def add_task_with_children(task_id): + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + sorted_rows.append(['---'] * len(row)) # Add separator row + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task) + + # Find all tasks that do not have a parent (top-level tasks) + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()]['Task ID'].unique() + + # Add top-level tasks and their children to the sorted rows + for task_id in top_level_tasks: + add_task_with_children(task_id) + + # Convert the list of sorted rows back to a DataFrame + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + + # Write the sorted DataFrame to a CSV file + sorted_df.to_csv(csv_output_file, index=False) + + # Write the sorted DataFrame to a TXT file + with open(txt_output_file, 'w', encoding='utf-8') as f: + for index, row in sorted_df.iterrows(): + f.write('\t'.join(map(str, row.values)) + '\n') + if all(cell == '---' for cell in row.values): + f.write('\n') # Add an extra newline after separator rows diff --git a/src/personal_notes/clickup_extract/v3.0clean_sorted.py b/src/personal_notes/clickup_extract/v3.0clean_sorted.py new file mode 100755 index 0000000..29f276b --- /dev/null +++ b/src/personal_notes/clickup_extract/v3.0clean_sorted.py @@ -0,0 +1,99 @@ +import os +import pandas as pd +import re + +#output looksl ike: unnecessary folders, hard to read data. + + +# Preparing clickup exported CSV file for cleaning unnecessary columns +csv_file = 'clickup2.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Specify the columns you want to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop the specified columns from the DataFrame +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get all unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Print or use the unique values as needed +print(unique_values) + +# Find all unique tags +all_tags = set() +for tags in df_sorted['Tags'].dropna(): + all_tags.update(tags.split(',')) + +# Write the modified DataFrame back to CSV and TXT files +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +for value in unique_values: + # Sanitize the string to replace problematic characters + sanitized_value = value.replace('/', '_') # Replace '/' with '_' + csv_output_file = os.path.join(output_dir, f'{sanitized_value}.csv') + txt_output_file = os.path.join(output_dir, f'{sanitized_value}.txt') + + # Filter the DataFrame based on the current value + filtered_df = df[df['List Name'] == value] + + # Drop additional columns from the filtered DataFrame + additional_columns_to_drop = ['Space Name', 'List Name'] # Replace with the names of columns you want to drop + filtered_df = filtered_df.drop(columns=additional_columns_to_drop) + + # Create a list to hold the final sorted rows + sorted_rows = [] + + # Function to add tasks and their children + def add_task_with_children(task_id): + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task) + + # Find all tasks that do not have a parent (top-level tasks) + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()]['Task ID'].unique() + + # Add top-level tasks and their children to the sorted rows + for task_id in top_level_tasks: + add_task_with_children(task_id) + + # Convert the list of sorted rows back to a DataFrame + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + + # Write the sorted DataFrame to a CSV file + sorted_df.to_csv(csv_output_file, index=False) + + # Write the sorted DataFrame to a TXT file + with open(txt_output_file, 'w', encoding='utf-8') as f: + for index, row in sorted_df.iterrows(): + f.write('\t'.join(map(str, row.values)) + '\n') + + # Write the DataFrame to tag-specific folders + for tag in all_tags: + # Escape special characters in the tag + escaped_tag = re.escape(tag.strip()) + tag_filtered_df = sorted_df[sorted_df['Tags'].str.contains(escaped_tag, na=False)] + if not tag_filtered_df.empty: + tag_dir = os.path.join(output_dir, tag.strip()) + os.makedirs(tag_dir, exist_ok=True) + tag_csv_output_file = os.path.join(tag_dir, f'{sanitized_value}.csv') + tag_txt_output_file = os.path.join(tag_dir, f'{sanitized_value}.txt') + + tag_filtered_df.to_csv(tag_csv_output_file, index=False) + + with open(tag_txt_output_file, 'w', encoding='utf-8') as f: + for index, row in tag_filtered_df.iterrows(): + f.write('\t'.join(map(str, row.values)) + '\n') + +print(f'Processed {len(df)} rows and saved them as .csv and .txt files in the "{output_dir}" directory and tag-specific subdirectories.') diff --git a/src/personal_notes/clickup_extract/v3.1.py b/src/personal_notes/clickup_extract/v3.1.py new file mode 100755 index 0000000..68fb915 --- /dev/null +++ b/src/personal_notes/clickup_extract/v3.1.py @@ -0,0 +1,110 @@ +import os +import pandas as pd +import re + +#output looks like: with unnecessary folders by the tags, clean data. + + +# Preparing clickup exported CSV file for cleaning unnecessary columns +csv_file = 'clickup2.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Specify the columns you want to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop the specified columns from the DataFrame +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get all unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Print or use the unique values as needed +print(unique_values) + +# Find all unique tags +all_tags = set() +for tags in df_sorted['Tags'].dropna(): + all_tags.update(tags.split(',')) + +# Write the modified DataFrame back to CSV and TXT files +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +for value in unique_values: + # Sanitize the string to replace problematic characters + sanitized_value = value.replace('/', '_') # Replace '/' with '_' + csv_output_file = os.path.join(output_dir, f'{sanitized_value}.csv') + txt_output_file = os.path.join(output_dir, f'{sanitized_value}.txt') + + # Filter the DataFrame based on the current value + filtered_df = df[df['List Name'] == value] + + # Drop additional columns from the filtered DataFrame + additional_columns_to_drop = ['Space Name', 'List Name'] # Replace with the names of columns you want to drop + filtered_df = filtered_df.drop(columns=additional_columns_to_drop) + + # Create a list to hold the final sorted rows + sorted_rows = [] + + # Function to add tasks and their children + def add_task_with_children(task_id): + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task) + + # Find all tasks that do not have a parent (top-level tasks) + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()]['Task ID'].unique() + + # Add top-level tasks and their children to the sorted rows + for task_id in top_level_tasks: + add_task_with_children(task_id) + + # Convert the list of sorted rows back to a DataFrame + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + + # Write the sorted DataFrame to a CSV file + sorted_df.to_csv(csv_output_file, index=False) + + # Write the sorted DataFrame to a TXT file without "Task ID" and "Parent ID" columns and removing NaN and [] + txt_columns_to_exclude = ['Task ID', 'Parent ID'] + sorted_df_txt = sorted_df.drop(columns=txt_columns_to_exclude) + sorted_df_txt = sorted_df_txt.replace(['NaN', '[]'], '') + + with open(txt_output_file, 'w', encoding='utf-8') as f: + for index, row in sorted_df_txt.iterrows(): + cleaned_row = [f"(({value}))" if pd.notna(value) and col == 'Task Content' else str(value) if pd.notna(value) else '' + for col, value in zip(sorted_df_txt.columns, row.values)] + f.write('\t'.join(cleaned_row) + '\n') + + # Write the DataFrame to tag-specific folders + for tag in all_tags: + # Escape special characters in the tag + escaped_tag = re.escape(tag.strip()) + tag_filtered_df = sorted_df[sorted_df['Tags'].str.contains(escaped_tag, na=False)] + if not tag_filtered_df.empty: + tag_dir = os.path.join(output_dir, tag.strip()) + os.makedirs(tag_dir, exist_ok=True) + tag_csv_output_file = os.path.join(tag_dir, f'{sanitized_value}.csv') + tag_txt_output_file = os.path.join(tag_dir, f'{sanitized_value}.txt') + + tag_filtered_df.to_csv(tag_csv_output_file, index=False) + + tag_filtered_df_txt = tag_filtered_df.drop(columns=txt_columns_to_exclude) + tag_filtered_df_txt = tag_filtered_df_txt.replace(['NaN', '[]'], '') + + with open(tag_txt_output_file, 'w', encoding='utf-8') as f: + for index, row in tag_filtered_df_txt.iterrows(): + cleaned_row = [f"(({value}))" if pd.notna(value) and col == 'Task Content' else str(value) if pd.notna(value) else '' + for col, value in zip(tag_filtered_df_txt.columns, row.values)] + f.write('\t'.join(cleaned_row) + '\n') + +print(f'Processed {len(df)} rows and saved them as .csv and .txt files in the "{output_dir}" directory and tag-specific subdirectories.') diff --git a/src/personal_notes/clickup_extract/v3.2.py b/src/personal_notes/clickup_extract/v3.2.py new file mode 100755 index 0000000..776ea18 --- /dev/null +++ b/src/personal_notes/clickup_extract/v3.2.py @@ -0,0 +1,84 @@ +import os +import pandas as pd +import re + +#output looks like: clean data. +# NEXT TBD improvement: create a folder for each parent ID, and txt file for each children that has attached parent ID + + +# Preparing clickup exported CSV file for cleaning unnecessary columns +csv_file = 'clickup10-25.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Specify the columns you want to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop the specified columns from the DataFrame +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get all unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Print or use the unique values as needed +print(unique_values) + +# Write the modified DataFrame back to CSV and TXT files +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +for value in unique_values: + # Sanitize the string to replace problematic characters + sanitized_value = value.replace('/', '_') # Replace '/' with '_' + csv_output_file = os.path.join(output_dir, f'{sanitized_value}.csv') + txt_output_file = os.path.join(output_dir, f'{sanitized_value}.txt') + + # Filter the DataFrame based on the current value + filtered_df = df[df['List Name'] == value] + + # Drop additional columns from the filtered DataFrame + additional_columns_to_drop = ['Space Name', 'List Name'] # Replace with the names of columns you want to drop + filtered_df = filtered_df.drop(columns=additional_columns_to_drop) + + # Create a list to hold the final sorted rows + sorted_rows = [] + + # Function to add tasks and their children + def add_task_with_children(task_id): + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task) + + # Find all tasks that do not have a parent (top-level tasks) + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()]['Task ID'].unique() + + # Add top-level tasks and their children to the sorted rows + for task_id in top_level_tasks: + add_task_with_children(task_id) + + # Convert the list of sorted rows back to a DataFrame + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + + # Write the sorted DataFrame to a CSV file + sorted_df.to_csv(csv_output_file, index=False) + + # Write the sorted DataFrame to a TXT file without "Task ID" and "Parent ID" columns and removing NaN and [] + txt_columns_to_exclude = ['Task ID', 'Parent ID'] + sorted_df_txt = sorted_df.drop(columns=txt_columns_to_exclude) + sorted_df_txt = sorted_df_txt.replace(['NaN', '[]'], '') + + with open(txt_output_file, 'w', encoding='utf-8') as f: + for index, row in sorted_df_txt.iterrows(): + cleaned_row = [f"(({value}))" if pd.notna(value) and col == 'Task Content' else str(value) if pd.notna(value) else '' + for col, value in zip(sorted_df_txt.columns, row.values)] + f.write('\t'.join(cleaned_row) + '\n') + +print(f'Processed {len(df)} rows and saved them as .csv and .txt files in the "{output_dir}" directory.') diff --git a/src/personal_notes/clickup_extract/v3.3.py b/src/personal_notes/clickup_extract/v3.3.py new file mode 100755 index 0000000..de6697b --- /dev/null +++ b/src/personal_notes/clickup_extract/v3.3.py @@ -0,0 +1,109 @@ +import os +import pandas as pd +import re + +#!!!!!!!!!!!!!!!!!!! QUITE neat. for the v3.4 version solve this problem: \n in txt files + +# Specify the CSV file path +csv_file = 'clickup10-25.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Strip whitespace from column names immediately after loading +df.columns = df.columns.str.strip() + +# Columns to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop specified columns +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Define output directory +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +# Define a maximum length for folder and file names (adjust as needed) +MAX_NAME_LENGTH = 100 + +# Utility function to sanitize names +def sanitize_name(name): + # Remove invalid characters and truncate + sanitized = re.sub(r'[<>:"/\\|?*]', '', str(name)) + return sanitized[:MAX_NAME_LENGTH] # Truncate if necessary + +# Process each unique List Name +for value in unique_values: + sanitized_list_name = sanitize_name(value) # Sanitize List Name + list_folder = os.path.join(output_dir, sanitized_list_name) + os.makedirs(list_folder, exist_ok=True) + + # Filter DataFrame based on the current List Name value + filtered_df = df[df['List Name'] == value] + + # Drop columns 'Space Name' and 'List Name' + filtered_df = filtered_df.drop(columns=['Space Name', 'List Name']) + + # Prepare a list to hold sorted rows + sorted_rows = [] + + # Define function to add tasks and their children + def add_task_with_children(task_id): + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task) + + # Find top-level tasks (no parent) and create folders for each + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()] + + for _, task_row in top_level_tasks.iterrows(): + task_id = task_row['Task ID'] + task_name = task_row['Task Name'] + + # Sanitize the task name for folder and file naming + sanitized_task_name = sanitize_name(task_name) # Sanitize Task Name + + # Create folder for the top-level task using the sanitized name + task_folder = os.path.join(list_folder, sanitized_task_name) + os.makedirs(task_folder, exist_ok=True) + + # Collect children of the top-level task + sorted_rows.clear() + add_task_with_children(task_id) + + # Convert to DataFrame + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + + # Name the text file based on the parent task's name + txt_output_file = os.path.join(task_folder, f'{sanitized_task_name}.txt') + + # Check for filename length and truncate if necessary + if len(txt_output_file) > 255: + txt_output_file = os.path.join(task_folder, f'{sanitized_task_name[:255]}.txt') + + txt_columns_to_exclude = ['Task ID', 'Parent ID'] + sorted_df_txt = sorted_df.drop(columns=txt_columns_to_exclude) + sorted_df_txt = sorted_df_txt.replace(['NaN', '[]'], '') + + # Write to the text file + with open(txt_output_file, 'w', encoding='utf-8') as f: + for _, row in sorted_df_txt.iterrows(): + cleaned_row = [f"(({val}))" if pd.notna(val) and col == 'Task Content' else str(val) if pd.notna(val) else '' + for col, val in zip(sorted_df_txt.columns, row.values)] + f.write('\t'.join(cleaned_row) + '\n') + f.write(" \n") # Add separator line after each child's content + f.write(" \n") # Add separator line after each child's content + f.write(" \n") # Add separator line after each child's content + f.write(" \n") # Add separator line after each child's content + +print(f'Processed {len(df)} rows and saved them in the "{output_dir}" directory with the specified folder structure.') diff --git a/src/personal_notes/clickup_extract/v3.4.0.py b/src/personal_notes/clickup_extract/v3.4.0.py new file mode 100755 index 0000000..c8b3ddb --- /dev/null +++ b/src/personal_notes/clickup_extract/v3.4.0.py @@ -0,0 +1,117 @@ +import os +import pandas as pd +import re +import glob + +# Specify the CSV file path +csv_file = 'clickup10-25.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Strip whitespace from column names immediately after loading +df.columns = df.columns.str.strip() + +# Columns to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop specified columns +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Define output directory +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +# Define a maximum length for folder and file names (adjust as needed) +MAX_NAME_LENGTH = 100 + +# Utility function to sanitize names +def sanitize_name(name): + # Remove invalid characters and truncate + sanitized = re.sub(r'[<>:"/\\|?*]', '', str(name)) + return sanitized[:MAX_NAME_LENGTH] # Truncate if necessary + +# Process each unique List Name +for value in unique_values: + sanitized_list_name = sanitize_name(value) # Sanitize List Name + list_folder = os.path.join(output_dir, sanitized_list_name) + os.makedirs(list_folder, exist_ok=True) + + # Filter DataFrame based on the current List Name value + filtered_df = df[df['List Name'] == value] + + # Drop columns 'Space Name' and 'List Name' + filtered_df = filtered_df.drop(columns=['Space Name', 'List Name']) + + # Prepare a list to hold sorted rows + sorted_rows = [] + + # Define function to add tasks and their children + def add_task_with_children(task_id): + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task) + + # Find top-level tasks (no parent) and create folders for each + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()] + + for _, task_row in top_level_tasks.iterrows(): + task_id = task_row['Task ID'] + task_name = task_row['Task Name'] + + # Sanitize the task name for folder and file naming + sanitized_task_name = sanitize_name(task_name) # Sanitize Task Name + + # Create folder for the top-level task using the sanitized name + task_folder = os.path.join(list_folder, sanitized_task_name) + os.makedirs(task_folder, exist_ok=True) + + # Collect children of the top-level task + sorted_rows.clear() + add_task_with_children(task_id) + + # Convert to DataFrame + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + + # Name the text file based on the parent task's name + txt_output_file = os.path.join(task_folder, f'{sanitized_task_name}.txt') + + # Check for filename length and truncate if necessary + if len(txt_output_file) > 255: + txt_output_file = os.path.join(task_folder, f'{sanitized_task_name[:255]}.txt') + + txt_columns_to_exclude = ['Task ID', 'Parent ID'] + sorted_df_txt = sorted_df.drop(columns=txt_columns_to_exclude) + + # Clean up specific values without regex + sorted_df_txt = sorted_df_txt.replace(['NaN', '[]'], '') + + # Remove any newline characters or multiple consecutive newlines that may still be present in each column + sorted_df_txt = sorted_df_txt.apply( + lambda col: col.map(lambda x: re.sub(r'\n+', ' ', str(x)) if pd.notna(x) else x) + ) + + + # Write to the text file + with open(txt_output_file, 'w', encoding='utf-8') as f: + for _, row in sorted_df_txt.iterrows(): + cleaned_row = [f"(({val}))" if pd.notna(val) and col == 'Task Content' else str(val) if pd.notna(val) else '' + for col, val in zip(sorted_df_txt.columns, row.values)] + f.write('\t'.join(cleaned_row) + '\n') + + # Add separator line after each child's content for clarity + f.write(" \n") # Add separator line after each child's content + f.write(" \n") # Add separator line after each child's content + f.write(" \n") # Add separator line after each child's content + f.write(" \n") # Add separator line after each child's content +print(f'Processed {len(df)} rows and saved them in the "{output_dir}" directory with the specified folder structure.') diff --git a/src/personal_notes/clickup_extract/v3.4.1.ALPHA.py b/src/personal_notes/clickup_extract/v3.4.1.ALPHA.py new file mode 100755 index 0000000..c6f54e2 --- /dev/null +++ b/src/personal_notes/clickup_extract/v3.4.1.ALPHA.py @@ -0,0 +1,119 @@ +import os +import pandas as pd +import re +import glob + +# Specify the CSV file path +csv_file = 'clickup10-25.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Strip whitespace from column names immediately after loading +df.columns = df.columns.str.strip() + +# Columns to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop specified columns +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Define output directory +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +# Define a maximum length for folder and file names (adjust as needed) +MAX_NAME_LENGTH = 100 + +# Utility function to sanitize names +def sanitize_name(name): + # Remove invalid characters and truncate + sanitized = re.sub(r'[<>:"/\\|?*]', '', str(name)).strip() # Remove invalid chars and leading/trailing whitespace + sanitized = sanitized.rstrip('.') # Remove trailing dots for cross-platform compatibility + return sanitized[:MAX_NAME_LENGTH] # Truncate if necessary + + +# Process each unique List Name +for value in unique_values: + sanitized_list_name = sanitize_name(value) # Sanitize List Name + list_folder = os.path.join(output_dir, sanitized_list_name) + os.makedirs(list_folder, exist_ok=True) + + # Filter DataFrame based on the current List Name value + filtered_df = df[df['List Name'] == value] + + # Drop columns 'Space Name' and 'List Name' + filtered_df = filtered_df.drop(columns=['Space Name', 'List Name']) + + # Prepare a list to hold sorted rows + sorted_rows = [] + + # Define function to add tasks and their children + def add_task_with_children(task_id): + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task) + + # Find top-level tasks (no parent) and create folders for each + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()] + + for _, task_row in top_level_tasks.iterrows(): + task_id = task_row['Task ID'] + task_name = task_row['Task Name'] + + # Sanitize the task name for folder and file naming + sanitized_task_name = sanitize_name(task_name) # Sanitize Task Name + + # Create folder for the top-level task using the sanitized name + task_folder = os.path.join(list_folder, sanitized_task_name) + os.makedirs(task_folder, exist_ok=True) + + # Collect children of the top-level task + sorted_rows.clear() + add_task_with_children(task_id) + + # Convert to DataFrame + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + + # Name the text file based on the parent task's name + txt_output_file = os.path.join(task_folder, f'{sanitized_task_name}.txt') + + # Check for filename length and truncate if necessary + if len(txt_output_file) > 255: + txt_output_file = os.path.join(task_folder, f'{sanitized_task_name[:255]}.txt') + + txt_columns_to_exclude = ['Task ID', 'Parent ID'] + sorted_df_txt = sorted_df.drop(columns=txt_columns_to_exclude) + + # Clean up specific values without regex + sorted_df_txt = sorted_df_txt.replace(['NaN', '[]'], '') + + # Remove any newline characters or multiple consecutive newlines that may still be present in each column + sorted_df_txt = sorted_df_txt.apply( + lambda col: col.map(lambda x: re.sub(r'\n+', ' ', str(x)) if pd.notna(x) else x) + ) + + + # Write to the text file + with open(txt_output_file, 'w', encoding='utf-8') as f: + for _, row in sorted_df_txt.iterrows(): + cleaned_row = [f"(({val}))" if pd.notna(val) and col == 'Task Content' else str(val) if pd.notna(val) else '' + for col, val in zip(sorted_df_txt.columns, row.values)] + f.write('\t'.join(cleaned_row) + '\n') + + # Add separator line after each child's content for clarity + f.write(" \n") # Add separator line after each child's content + f.write(" \n") # Add separator line after each child's content + f.write(" \n") # Add separator line after each child's content + f.write(" \n") # Add separator line after each child's content +print(f'Processed {len(df)} rows and saved them in the "{output_dir}" directory with the specified folder structure.') diff --git a/src/personal_notes/clickup_extract/v4.1.1.ALPHA.py b/src/personal_notes/clickup_extract/v4.1.1.ALPHA.py new file mode 100755 index 0000000..df125dd --- /dev/null +++ b/src/personal_notes/clickup_extract/v4.1.1.ALPHA.py @@ -0,0 +1,118 @@ +import os +import pandas as pd +import re +import glob + +#TBD: radau neatitikima: sub_task folderiuose esantys txt failai turi ne tik sau priklausanti turini, bet ir tarsi i juos appendintas is kitu sub_tasku turinys + +# Specify the CSV file path +csv_file = 'clickup10-25.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Strip whitespace from column names immediately after loading +df.columns = df.columns.str.strip() + +# Columns to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop specified columns +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Define output directory +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +# Define a maximum length for folder and file names (adjust as needed) +MAX_NAME_LENGTH = 100 + +# Utility function to sanitize names +def sanitize_name(name): + # Remove invalid characters and truncate + sanitized = re.sub(r'[<>:"/\\|?*]', '', str(name)).strip() # Remove invalid chars and leading/trailing whitespace + sanitized = sanitized.rstrip('.') # Remove trailing dots for cross-platform compatibility + sanitized = sanitized.replace('.', '') # Remove all periods for compatibility + return sanitized[:MAX_NAME_LENGTH] # Truncate if necessary + + +# Process each unique List Name +for value in unique_values: + sanitized_list_name = sanitize_name(value) # Sanitize List Name + list_folder = os.path.join(output_dir, sanitized_list_name) + os.makedirs(list_folder, exist_ok=True) + + # Filter DataFrame based on the current List Name value + filtered_df = df[df['List Name'] == value] + + # Drop columns 'Space Name' and 'List Name' + filtered_df = filtered_df.drop(columns=['Space Name', 'List Name']) + + # Prepare a list to hold sorted rows + sorted_rows = [] + + # Define function to add tasks and their children + def add_task_with_children(task_id, parent_folder, is_child=False): + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + task_name = row[filtered_df.columns.get_loc("Task Name")] + sanitized_task_name = sanitize_name(task_name) # Sanitize Task Name + + # If it's a child task, create a subfolder; if it's top-level, use the parent_folder directly + if is_child: + subtask_folder = os.path.join(parent_folder, sanitized_task_name) + os.makedirs(subtask_folder, exist_ok=True) + target_folder = subtask_folder + else: + target_folder = parent_folder + + # Collect children of the current task + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task, target_folder, is_child=True) + + # Create and write to the text file inside the target folder + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + txt_output_file = os.path.join(target_folder, f'{sanitized_task_name}.txt') + + # Exclude specific columns and clean the content + txt_columns_to_exclude = ['Task ID', 'Parent ID'] + sorted_df_txt = sorted_df.drop(columns=txt_columns_to_exclude) + sorted_df_txt = sorted_df_txt.replace(['NaN', '[]'], '') + sorted_df_txt = sorted_df_txt.apply( + lambda col: col.map(lambda x: re.sub(r'\n+', ' ', str(x)) if pd.notna(x) else x) + ) + + with open(txt_output_file, 'w', encoding='utf-8') as f: + for _, row in sorted_df_txt.iterrows(): + cleaned_row = [f"(({val}))" if pd.notna(val) and col == 'Task Content' else str(val) if pd.notna(val) else '' + for col, val in zip(sorted_df_txt.columns, row.values)] + f.write('\t'.join(cleaned_row) + '\n') + # Separator lines for clarity + f.write(" \n") # Add separator line for clarity + + # Find top-level tasks (no parent) and create folders for each + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()] + for _, task_row in top_level_tasks.iterrows(): + task_id = task_row['Task ID'] + task_name = task_row['Task Name'] + + # Sanitize and create folder for the top-level task + sanitized_task_name = sanitize_name(task_name) + task_folder = os.path.join(list_folder, sanitized_task_name) + os.makedirs(task_folder, exist_ok=True) + + # Add tasks with the new folder structure + sorted_rows.clear() + add_task_with_children(task_id, task_folder) + +print(f'Processed {len(df)} rows and saved them in the "{output_dir}" directory with the specified folder structure.') + diff --git a/src/personal_notes/clickup_extract/v4.1.2.FINAL.py b/src/personal_notes/clickup_extract/v4.1.2.FINAL.py new file mode 100755 index 0000000..d30a0d7 --- /dev/null +++ b/src/personal_notes/clickup_extract/v4.1.2.FINAL.py @@ -0,0 +1,119 @@ +import os +import pandas as pd +import re +import glob + +#TBD: radau neatitikima: sub_task folderiuose esantys txt failai turi ne tik sau priklausanti turini, bet ir tarsi i juos appendintas is kitu sub_tasku turinys + +# Specify the CSV file path +csv_file = 'clickup10-25.csv' # Replace with the path to your CSV file +df = pd.read_csv(csv_file) + +# Strip whitespace from column names immediately after loading +df.columns = df.columns.str.strip() + +# Columns to delete (excluding "Task ID" and "Parent ID") +columns_to_delete = ['Attachments', 'Date Created', 'Date Created Text', 'Due Date', 'Due Date Text', + 'Start Date', 'Start Date Text', 'Assignees', 'Folder Name', 'Time Estimated', + 'Time Estimated Text', 'Checklists', 'Comments', 'Assigned Comments', 'Time Spent', + 'Time Spent Text', 'Rolled Up Time', 'Rolled Up Time Text', 'Priority', 'Status'] + +# Drop specified columns +df = df.drop(columns=columns_to_delete) + +# Sort the DataFrame by the "Space Name" column +df_sorted = df.sort_values(by='Space Name') + +# Get unique values in the "List Name" column +unique_values = df_sorted['List Name'].unique() + +# Define output directory +output_dir = 'output' +os.makedirs(output_dir, exist_ok=True) + +# Define a maximum length for folder and file names (adjust as needed) +MAX_NAME_LENGTH = 100 + +# Utility function to sanitize names +def sanitize_name(name): + # Remove invalid characters and truncate + sanitized = re.sub(r'[<>:"/\\|?*]', '', str(name)).strip() # Remove invalid chars and leading/trailing whitespace + sanitized = sanitized.rstrip('.') # Remove trailing dots for cross-platform compatibility + sanitized = sanitized.replace('.', '') # Remove all periods for compatibility + return sanitized[:MAX_NAME_LENGTH] # Truncate if necessary + + +# Process each unique List Name +for value in unique_values: + sanitized_list_name = sanitize_name(value) # Sanitize List Name + list_folder = os.path.join(output_dir, sanitized_list_name) + os.makedirs(list_folder, exist_ok=True) + + # Filter DataFrame based on the current List Name value + filtered_df = df[df['List Name'] == value] + + # Drop columns 'Space Name' and 'List Name' + filtered_df = filtered_df.drop(columns=['Space Name', 'List Name']) + + # Define function to add tasks and their children + def add_task_with_children(task_id, parent_folder, is_child=False): + # Clear sorted_rows to isolate data for each task or subtask + sorted_rows = [] + + # Collect rows for the current task + task_rows = filtered_df[filtered_df['Task ID'] == task_id].values.tolist() + for row in task_rows: + sorted_rows.append(row) + task_name = row[filtered_df.columns.get_loc("Task Name")] + sanitized_task_name = sanitize_name(task_name) # Sanitize Task Name + + # If it's a child task, create a subfolder; if it's top-level, use the parent_folder directly + if is_child: + subtask_folder = os.path.join(parent_folder, sanitized_task_name) + os.makedirs(subtask_folder, exist_ok=True) + target_folder = subtask_folder + else: + target_folder = parent_folder + + # Collect children of the current task + child_tasks = filtered_df[filtered_df['Parent ID'] == task_id]['Task ID'].unique() + for child_task in child_tasks: + add_task_with_children(child_task, target_folder, is_child=True) + + # Convert to DataFrame + sorted_df = pd.DataFrame(sorted_rows, columns=filtered_df.columns) + txt_output_file = os.path.join(target_folder, f'{sanitized_task_name}.txt') + + # Exclude specific columns and clean the content + txt_columns_to_exclude = ['Task ID', 'Parent ID'] + sorted_df_txt = sorted_df.drop(columns=txt_columns_to_exclude) + sorted_df_txt = sorted_df_txt.replace(['NaN', '[]'], '') + sorted_df_txt = sorted_df_txt.apply( + lambda col: col.map(lambda x: re.sub(r'\n+', ' ', str(x)) if pd.notna(x) else x) + ) + + # Write data for the current task/subtask to its respective text file + with open(txt_output_file, 'w', encoding='utf-8') as f: + for _, row in sorted_df_txt.iterrows(): + cleaned_row = [f"(({val}))" if pd.notna(val) and col == 'Task Content' else str(val) if pd.notna(val) else '' + for col, val in zip(sorted_df_txt.columns, row.values)] + f.write('\t'.join(cleaned_row) + '\n') + # Separator lines for clarity + f.write(" \n") # Add separator line for clarity + + # Find top-level tasks (no parent) and create folders for each + top_level_tasks = filtered_df[filtered_df['Parent ID'].isna()] + for _, task_row in top_level_tasks.iterrows(): + task_id = task_row['Task ID'] + task_name = task_row['Task Name'] + + # Sanitize and create folder for the top-level task + sanitized_task_name = sanitize_name(task_name) + task_folder = os.path.join(list_folder, sanitized_task_name) + os.makedirs(task_folder, exist_ok=True) + + # Add tasks with the new folder structure + add_task_with_children(task_id, task_folder) + +print(f'Processed {len(df)} rows and saved them in the "{output_dir}" directory with the specified folder structure.') + diff --git a/src/personal_notes/discord_extract/__pycache__/json.cpython-311.pyc b/src/personal_notes/discord_extract/__pycache__/json.cpython-311.pyc new file mode 100755 index 0000000..b6d1902 Binary files /dev/null and b/src/personal_notes/discord_extract/__pycache__/json.cpython-311.pyc differ diff --git a/src/personal_notes/discord_extract/__pycache__/responses.cpython-311.pyc b/src/personal_notes/discord_extract/__pycache__/responses.cpython-311.pyc new file mode 100755 index 0000000..55fc46c Binary files /dev/null and b/src/personal_notes/discord_extract/__pycache__/responses.cpython-311.pyc differ diff --git a/src/personal_notes/discord_extract/cleaning.py b/src/personal_notes/discord_extract/cleaning.py new file mode 100755 index 0000000..093d4e9 --- /dev/null +++ b/src/personal_notes/discord_extract/cleaning.py @@ -0,0 +1,49 @@ +import json +import pandas as pd + +""" +# After posts are scraped, time to look for duplicates and remove them: +def remove_duplicates(json_file): + # Read the contents of the JSON file into a list + with open(json_file, 'r') as file: + lines = file.readlines() + + # Initialize lists to store message data + ids = [] + channels = [] + contents = [] + timestamps = [] + + # Extract message data from each line in the JSON file + for line in lines: + message = json.loads(line) + message_id = next(iter(message)) # Get the message ID + ids.append(message_id) + channels.append(message[message_id]['channel']) + contents.append(message[message_id]['content']) + timestamps.append(message[message_id]['timestamp']) + + # Create a DataFrame from the extracted data + df = pd.DataFrame({'id': ids, 'channel': channels, 'content': contents, 'timestamp': timestamps}) + + # Drop duplicates based on message ID + clean_df = df.drop_duplicates(subset=['id']) + + # Open the new file to save cleaned messages + with open('clean_messages.json', 'w') as file: + # Serialize each message individually and write them to the file + for _, message_row in clean_df.iterrows(): + message_data = { + 'id': message_row['id'], + 'channel': message_row['channel'], + 'content': message_row['content'], + 'timestamp': message_row['timestamp'] + } + json.dump(message_data, file) + file.write('\n') # Add newline for readability + +# Example usage: +remove_duplicates('messages.json') +""" + +# working with final data \ No newline at end of file diff --git a/src/personal_notes/discord_extract/disc_del_channels.py b/src/personal_notes/discord_extract/disc_del_channels.py new file mode 100755 index 0000000..a27ca6b --- /dev/null +++ b/src/personal_notes/discord_extract/disc_del_channels.py @@ -0,0 +1,94 @@ +import os +from dotenv import load_dotenv +import discord +from discord import Intents, Client +from discord.errors import Forbidden +import asyncio + +#instead of deleting posts, it's more efficient to delete channels and re-create them. !!! Manually insert category name in the code + +# Load bot token from .env file +load_dotenv() +TOKEN = os.getenv('DISCORD_TOKEN') + +# Set up intents +intents = Intents.default() +intents.messages = True + +# Define permission flags +PERMISSION_READ_MESSAGES = 1 << 6 # 65536 uneccessary? +PERMISSION_MANAGE_MESSAGES = 1 << 13 # 8192 uneccessary? +PERMISSION_READ_MESSAGE_HISTORY = 1 << 11 # 73728 uneccessary? +PERMISSION_MANAGE_CHANNELS = 1 << 4 # 16 + +# Calculate permissions integer +permissions = ( + PERMISSION_READ_MESSAGES | PERMISSION_MANAGE_MESSAGES | + PERMISSION_READ_MESSAGE_HISTORY | PERMISSION_MANAGE_CHANNELS +) + +# Initialize the Discord client with permissions +client = Client(intents=intents, permissions=permissions) + +# Dictionary to store deleted channel names and their positions in the category +deleted_channels = {} + +async def delete_and_recreate_channels(category): + global deleted_channels + try: + # Fetch the channels in the specified category + channels = category.channels + for channel in channels: + # Store the channel name and its position before deleting + deleted_channels[channel.name] = channel.position + + # Delete the channel + await channel.delete() + except Forbidden as e: + print(f"Permission error: {e}") + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.retry_after + print(f"We are being rate limited. Retrying in {retry_after} seconds.") + await asyncio.sleep(retry_after) + await delete_and_recreate_channels(category) + else: + print(f"HTTP error: {e}") + except Exception as e: + print(f"An error occurred: {e}") + +async def recreate_channels(category): + global deleted_channels + try: + # Iterate over the deleted channel names and positions + for channel_name, position in deleted_channels.items(): + # Create the channel in the specified category + await category.create_text_channel(name=channel_name, position=position) + except Forbidden as e: + print(f"Permission error: {e}") + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.retry_after + print(f"We are being rate limited. Retrying in {retry_after} seconds.") + await asyncio.sleep(retry_after) + await recreate_channels(category) + else: + print(f"HTTP error: {e}") + except Exception as e: + print(f"An error occurred: {e}") + +@client.event +async def on_ready(): + print(f'{client.user} has connected to Discord!') + print('Monitoring channels...') + for guild in client.guilds: + print(f'Guild: {guild.name}') + for category in guild.categories: + if category.name == 'Challenges for growth': # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Manually insert category name in the code + print(f'Category: {category.name}') + await delete_and_recreate_channels(category) + await recreate_channels(category) + await client.close() + +# Run the bot +client.run(TOKEN) diff --git a/src/personal_notes/discord_extract/disc_del_only.py b/src/personal_notes/discord_extract/disc_del_only.py new file mode 100755 index 0000000..176c2bc --- /dev/null +++ b/src/personal_notes/discord_extract/disc_del_only.py @@ -0,0 +1,64 @@ +import os +from dotenv import load_dotenv +import discord +from discord import Intents, Client +from discord.errors import Forbidden +import asyncio + +# Load bot token from .env file +load_dotenv() +TOKEN = os.getenv('DISCORD_TOKEN') + +# Set up intents +intents = Intents.default() +intents.messages = True + +# Define permission flags +PERMISSION_READ_MESSAGES = 1 << 6 # 65536 uneccessary? +PERMISSION_MANAGE_MESSAGES = 1 << 13 # 8192 uneccessary? +PERMISSION_READ_MESSAGE_HISTORY = 1 << 11 # 73728 uneccessary? +PERMISSION_MANAGE_CHANNELS = 1 << 4 # 16 + +# Calculate permissions integer +permissions = ( + PERMISSION_READ_MESSAGES | PERMISSION_MANAGE_MESSAGES | + PERMISSION_READ_MESSAGE_HISTORY | PERMISSION_MANAGE_CHANNELS +) + +# Initialize the Discord client with permissions +client = Client(intents=intents, permissions=permissions) + +async def delete_channels(category): + try: + # Fetch the channels in the specified category + channels = category.channels + for channel in channels: + # Delete the channel + await channel.delete() + except Forbidden as e: + print(f"Permission error: {e}") + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.retry_after + print(f"We are being rate limited. Retrying in {retry_after} seconds.") + await asyncio.sleep(retry_after) + await delete_channels(category) + else: + print(f"HTTP error: {e}") + except Exception as e: + print(f"An error occurred: {e}") + +@client.event +async def on_ready(): + print(f'{client.user} has connected to Discord!') + print('Monitoring channels...') + for guild in client.guilds: + print(f'Guild: {guild.name}') + for category in guild.categories: + if category.name == 'Noble': # Manually insert category name in the code + print(f'Category: {category.name}') + await delete_channels(category) + await client.close() + +# Run the bot +client.run(TOKEN) diff --git a/src/personal_notes/discord_extract/disc_extract_c.py b/src/personal_notes/discord_extract/disc_extract_c.py new file mode 100755 index 0000000..11051c9 --- /dev/null +++ b/src/personal_notes/discord_extract/disc_extract_c.py @@ -0,0 +1,181 @@ +import os +import discord +import json +import asyncio +from dotenv import load_dotenv +from discord import Intents, Client +from discord.errors import Forbidden, HTTPException +import pandas as pd +from collections import deque +import time + +#this bot scrapes chosen categories. !!! manually choose categories + +# Load bot token from .env file +load_dotenv() +TOKEN = os.getenv('DISCORD_TOKEN') + +# Set up intents +intents = Intents.default() +intents.messages = True + +# Define permission flags +PERMISSION_READ_MESSAGES = 1 << 6 # 65536 +PERMISSION_MANAGE_MESSAGES = 1 << 13 # 8192 +PERMISSION_READ_MESSAGE_HISTORY = 1 << 11 # 73728 + +# Calculate permissions integer +permissions = PERMISSION_READ_MESSAGES | PERMISSION_MANAGE_MESSAGES | PERMISSION_READ_MESSAGE_HISTORY + +# Initialize the Discord client with permissions +client = Client(intents=intents, permissions=permissions) + +# Define rate limit settings +RATE_LIMIT_THRESHOLD = 3 # Maximum number of requests per second +RATE_LIMIT_BUCKET = RATE_LIMIT_THRESHOLD # Initial token bucket capacity +categories_to_monitor = ['Aktualijos'] #!!! manually choose categories + +# Initialize request counter +request_count = 0 + +async def monitor_channel(channel): + global RATE_LIMIT_BUCKET + global request_count + try: + # Increment request count for each request + request_count += 1 + + # Check if there are enough tokens in the bucket + if RATE_LIMIT_BUCKET > 0: + # Make the request + async for msg in channel.history(limit=None): + # Create a dictionary to store message data + message_data = { + 'id': msg.id, + 'category': channel.category.name, + 'channel': channel.name, + 'content': msg.content, + 'timestamp': msg.created_at.strftime('%Y-%m-%d %H:%M') + } + + # Serialize message data into a string + message_str = json.dumps({msg.id: message_data}) + + # Save message data to the file + with open('messages.json', 'a') as file: + file.write(message_str + '\n') # Write message and add newline for readability + + # Decrement the token bucket + RATE_LIMIT_BUCKET -= 1 + else: + # Wait until tokens refill in the bucket + await asyncio.sleep(1) + RATE_LIMIT_BUCKET = RATE_LIMIT_THRESHOLD # Refill the token bucket + await monitor_channel(channel) + + except Forbidden as e: + print(f"Permission error: {e}") + except HTTPException as e: + # Check if the exception is due to rate limiting + if e.status == 429: + retry_after = e.retry_after #testing + print(f"We are being rate limited. We could retry in {retry_after} seconds.") #testing + print(f"We are being rate limited. Exiting for now.") + await client.close() # Exit the program + else: + print(f"HTTP error: {e}") + except Exception as e: + print(f"An error occurred: {e}") + +@client.event +async def on_ready(): + global request_count + print(f'{client.user} has connected to Discord!') + print('Monitoring channels...') + + # Record the start time + start_time = time.time() + + for guild in client.guilds: + print(f'Guild: {guild.name}') + for category in guild.categories: + if category.name in categories_to_monitor: + print(f'Category: {category.name}') + for channel in category.channels: + if isinstance(channel, discord.TextChannel): + await monitor_channel(channel) + + # Calculate and display the elapsed time + elapsed_time = time.time() - start_time + print(f"Total requests made: {request_count}") + print(f"Total time elapsed: {elapsed_time} seconds") + + await client.close() + +# Run the bot +client.run(TOKEN) + + +# After posts are scraped, time to look for duplicates and remove them: +def remove_duplicates(json_file): + # Read the contents of the JSON file into a list + with open(json_file, 'r') as file: + lines = file.readlines() + + # Initialize lists to store message data + ids = [] + category = [] + channels = [] + contents = [] + timestamps = [] + + # Extract message data from each line in the JSON file + for line in lines: + message = json.loads(line) + message_id = next(iter(message)) # Get the message ID + ids.append(message_id) + category.append(message[message_id]['category']) + channels.append(message[message_id]['channel']) + contents.append(message[message_id]['content']) + timestamps.append(message[message_id]['timestamp']) + + # Create a DataFrame from the extracted data + df = pd.DataFrame({'id': ids, 'category': category, 'channel': channels, 'content': contents, 'timestamp': timestamps}) + + # Drop duplicates based on message ID + clean_df = df.drop_duplicates(subset=['id']) + + # Open the file to append cleaned messages + # Set to store unique message IDs encountered so far + unique_message_ids = set() + + # Open the file to append cleaned messages + with open('clean_messages.json', 'a') as file: + # Read existing messages to populate unique_message_ids set + with open('clean_messages.json', 'r') as existing_file: + for line in existing_file: + try: + existing_message = json.loads(line) + unique_message_ids.add(existing_message['id']) + except json.JSONDecodeError as e: + print(f"Error loading JSON data: {e}. Skipping this line and continuing.") + + # Serialize each message individually and write them to the file if the ID is not already present + for _, message_row in clean_df.iterrows(): + message_id = message_row['id'] + if message_id not in unique_message_ids: + message_data = { + 'id': message_id, + 'category': message_row['category'], + 'channel': message_row['channel'], + 'content': message_row['content'], + 'timestamp': message_row['timestamp'] + } + json.dump(message_data, file) + file.write('\n') # Add newline for readability + unique_message_ids.add(message_id) # Add the ID to the set of unique IDs + + + +# Example usage: +remove_duplicates('messages.json') diff --git a/src/personal_notes/discord_extract/disc_extract_s.py b/src/personal_notes/discord_extract/disc_extract_s.py new file mode 100755 index 0000000..457106e --- /dev/null +++ b/src/personal_notes/discord_extract/disc_extract_s.py @@ -0,0 +1,177 @@ +import os +import discord +import json +import asyncio +from dotenv import load_dotenv +from discord import Intents, Client +from discord.errors import Forbidden, HTTPException +import pandas as pd +from collections import deque +import time + +#this bot scrapes whole server + +# Load bot token from .env file +load_dotenv() +TOKEN = os.getenv('DISCORD_TOKEN') + +# Set up intents +intents = Intents.default() +intents.messages = True + +# Define permission flags +PERMISSION_READ_MESSAGES = 1 << 6 # 65536 +PERMISSION_MANAGE_MESSAGES = 1 << 13 # 8192 +PERMISSION_READ_MESSAGE_HISTORY = 1 << 11 # 73728 + +# Calculate permissions integer +permissions = PERMISSION_READ_MESSAGES | PERMISSION_MANAGE_MESSAGES | PERMISSION_READ_MESSAGE_HISTORY + +# Initialize the Discord client with permissions +client = Client(intents=intents, permissions=permissions) + +# Define rate limit settings +RATE_LIMIT_THRESHOLD = 3 # Maximum number of requests per second +RATE_LIMIT_BUCKET = RATE_LIMIT_THRESHOLD # Initial token bucket capacity + +# Initialize request counter +request_count = 0 + +async def monitor_channel(channel): + global RATE_LIMIT_BUCKET + global request_count + try: + # Increment request count for each request + request_count += 1 + + # Check if there are enough tokens in the bucket + if RATE_LIMIT_BUCKET > 0: + # Make the request + async for msg in channel.history(limit=None): + # Create a dictionary to store message data + message_data = { + 'id': msg.id, + 'category': channel.category.name, + 'channel': channel.name, + 'content': msg.content, + 'timestamp': msg.created_at.strftime('%Y-%m-%d %H:%M') + } + + # Serialize message data into a string + message_str = json.dumps({msg.id: message_data}) + + # Save message data to the file + with open('messages.json', 'a') as file: + file.write(message_str + '\n') # Write message and add newline for readability + + # Decrement the token bucket + RATE_LIMIT_BUCKET -= 1 + else: + # Wait until tokens refill in the bucket + await asyncio.sleep(1) + RATE_LIMIT_BUCKET = RATE_LIMIT_THRESHOLD # Refill the token bucket + await monitor_channel(channel) + + except Forbidden as e: + print(f"Permission error: {e}") + except HTTPException as e: + # Check if the exception is due to rate limiting + if e.status == 429: + retry_after = e.retry_after #testing + print(f"We are being rate limited. We could retry in {retry_after} seconds.") #testing + print(f"We are being rate limited. Exiting for now.") + await client.close() # Exit the program + else: + print(f"HTTP error: {e}") + except Exception as e: + print(f"An error occurred: {e}") + +@client.event +async def on_ready(): + global request_count + print(f'{client.user} has connected to Discord!') + print('Monitoring channels...') + + # Record the start time + start_time = time.time() + + for guild in client.guilds: + print(f'Guild: {guild.name}') + for channel in guild.channels: + if isinstance(channel, discord.TextChannel): + await monitor_channel(channel) + + # Calculate and display the elapsed time + elapsed_time = time.time() - start_time + print(f"Total requests made: {request_count}") + print(f"Total time elapsed: {elapsed_time} seconds") + + await client.close() + +# Run the bot +client.run(TOKEN) + + +# After posts are scraped, time to look for duplicates and remove them: +def remove_duplicates(json_file): + # Read the contents of the JSON file into a list + with open(json_file, 'r') as file: + lines = file.readlines() + + # Initialize lists to store message data + ids = [] + category = [] + channels = [] + contents = [] + timestamps = [] + + # Extract message data from each line in the JSON file + for line in lines: + message = json.loads(line) + message_id = next(iter(message)) # Get the message ID + ids.append(message_id) + category.append(message[message_id]['category']) + channels.append(message[message_id]['channel']) + contents.append(message[message_id]['content']) + timestamps.append(message[message_id]['timestamp']) + + # Create a DataFrame from the extracted data + df = pd.DataFrame({'id': ids, 'category': category, 'channel': channels, 'content': contents, 'timestamp': timestamps}) + + # Drop duplicates based on message ID + clean_df = df.drop_duplicates(subset=['id']) + + # Open the file to append cleaned messages + # Set to store unique message IDs encountered so far + unique_message_ids = set() + + # Open the file to append cleaned messages + with open('clean_messages.json', 'a') as file: + # Read existing messages to populate unique_message_ids set + with open('clean_messages.json', 'r') as existing_file: + for line in existing_file: + try: + existing_message = json.loads(line) + unique_message_ids.add(existing_message['id']) + except json.JSONDecodeError as e: + print(f"Error loading JSON data: {e}. Skipping this line and continuing.") + + # Serialize each message individually and write them to the file if the ID is not already present + for _, message_row in clean_df.iterrows(): + message_id = message_row['id'] + if message_id not in unique_message_ids: + message_data = { + 'id': message_id, + 'category': message_row['category'], + 'channel': message_row['channel'], + 'content': message_row['content'], + 'timestamp': message_row['timestamp'] + } + json.dump(message_data, file) + file.write('\n') # Add newline for readability + unique_message_ids.add(message_id) # Add the ID to the set of unique IDs + + + +# Example usage: +remove_duplicates('messages.json') \ No newline at end of file diff --git a/src/personal_notes/discord_extract/json3.py b/src/personal_notes/discord_extract/json3.py new file mode 100755 index 0000000..9c46927 --- /dev/null +++ b/src/personal_notes/discord_extract/json3.py @@ -0,0 +1,49 @@ +import os +import json +import pandas as pd + +# Function to export data into both txt and json files +def export_data(channel, category, content_list): + # Create a folder for the category if it doesn't exist + category_folder = os.path.join('categories', category) + os.makedirs(category_folder, exist_ok=True) + + # Create a folder for the channel + channel_folder = os.path.join(category_folder, channel) + os.makedirs(channel_folder, exist_ok=True) + + # Export content to text file + text_file_path = os.path.join(channel_folder, 'content.txt') + with open(text_file_path, 'w', encoding='utf-8') as text_file: + for content in content_list: + if content and content != ".": + text_file.write(content + '\n') + + # Export content to json file + json_file_path = os.path.join(channel_folder, 'content.json') + with open(json_file_path, 'w', encoding='utf-8') as json_file: + json.dump(content_list, json_file, indent=4) + +# Load the messages from the JSON file +with open('clean_messages.json', 'r', encoding='utf-8') as file: + messages = [json.loads(line) for line in file] + +# Create a DataFrame from the messages +df = pd.DataFrame(messages) + +# Sort the DataFrame by 'channel' and 'timestamp' +df.sort_values(by=['channel', 'timestamp'], inplace=True) + +# Group the messages by the 'channel' column +grouped = df.groupby('channel') + +# Iterate over each group (channel) +for channel, group_df in grouped: + # Get the category of the channel + category = group_df['category'].iloc[0] # Assuming 'category' is a column in the DataFrame + + # Extract content from the group dataframe + content_list = list(group_df['content']) + + # Export data to text and json files + export_data(channel, category, content_list) diff --git a/src/personal_notes/discord_extract/json_to_txt.py b/src/personal_notes/discord_extract/json_to_txt.py new file mode 100755 index 0000000..9f6fcd7 --- /dev/null +++ b/src/personal_notes/discord_extract/json_to_txt.py @@ -0,0 +1,43 @@ +import os +import json +import pandas as pd + + + +# Load the messages from the JSON file +with open('clean_messages.json', 'r', encoding='utf-8') as file: + messages = [json.loads(line) for line in file] + +# Create a DataFrame from the messages +df = pd.DataFrame(messages) + +# Sort the DataFrame by 'channel' and 'timestamp' +df.sort_values(by=['channel', 'timestamp'], inplace=True) + +# Group the messages by the 'channel' column +grouped = df.groupby('channel') + +# Create a folder to store the text files for categories +os.makedirs('categories', exist_ok=True) + +# Iterate over each group (channel) +for channel, group_df in grouped: + # Get the category of the channel + category = group_df['category'].iloc[0] # Assuming 'category' is a column in the DataFrame + + # Create a folder for the category if it doesn't exist + category_folder = os.path.join('categories', category) + os.makedirs(category_folder, exist_ok=True) + + # Create a folder for the channel + channel_folder = os.path.join(category_folder, channel) + os.makedirs(channel_folder, exist_ok=True) + + # Create a text file for the channel's content + text_file_path = os.path.join(channel_folder, 'content.txt') + + # Write the non-empty content strings to the text file + with open(text_file_path, 'w', encoding='utf-8') as text_file: + for content in group_df['content']: + if content and content != ".": + text_file.write(content + '\n') diff --git a/src/personal_notes/discord_extract/json_to_txt2.py b/src/personal_notes/discord_extract/json_to_txt2.py new file mode 100755 index 0000000..bba8115 --- /dev/null +++ b/src/personal_notes/discord_extract/json_to_txt2.py @@ -0,0 +1,49 @@ +import os +import json +import pandas as pd +import re + +#next: use regex for <#832925562611302400> format. either show these numbers as letters and tags or don't show at all + +# Load the messages from the JSON file +with open('clean_messages.json', 'r', encoding='utf-8') as file: + messages = [json.loads(line) for line in file] + +# Create a DataFrame from the messages +df = pd.DataFrame(messages) + +# Sort the DataFrame by 'channel' and 'timestamp' +df.sort_values(by=['channel', 'timestamp'], inplace=True) + +# Replace the Discord ID format (<#832925562611302400>) with an empty string or a custom string +def replace_discord_id(content, replacement=''): + return re.sub(r'<#\d+>', replacement, content) +df['content'] = df['content'].apply(replace_discord_id) + +# Group the messages by the 'channel' column +grouped = df.groupby('channel') + +# Create a folder to store the text files for categories +os.makedirs('categories', exist_ok=True) + +# Iterate over each group (channel) +for channel, group_df in grouped: + # Get the category of the channel + category = group_df['category'].iloc[0] # Assuming 'category' is a column in the DataFrame + + # Create a folder for the category if it doesn't exist + category_folder = os.path.join('categories', category) + os.makedirs(category_folder, exist_ok=True) + + # Create a folder for the channel + channel_folder = os.path.join(category_folder, channel) + os.makedirs(channel_folder, exist_ok=True) + + # Create a text file for the channel's content + text_file_path = os.path.join(channel_folder, 'content.txt') + + # Write the non-empty content strings to the text file + with open(text_file_path, 'w', encoding='utf-8') as text_file: + for content in group_df['content']: + if content and content != ".": + text_file.write(content + '\n') diff --git a/src/personal_notes/discord_extract/responses.py b/src/personal_notes/discord_extract/responses.py new file mode 100755 index 0000000..4c2a606 --- /dev/null +++ b/src/personal_notes/discord_extract/responses.py @@ -0,0 +1,2 @@ +def get_response(user_input: str) -> str: + raise NotImplementedError("Code is missing") \ No newline at end of file diff --git a/src/personal_notes/discord_extract/scheme.txt b/src/personal_notes/discord_extract/scheme.txt new file mode 100755 index 0000000..7fc729d --- /dev/null +++ b/src/personal_notes/discord_extract/scheme.txt @@ -0,0 +1,10 @@ + +The extraction code scheme: +1. setting up a bot in the server https://www.youtube.com/watch?v=UYJDKSah-Ww +pip install discord +pip install python-dotenv +2. Read posts, copy and store each post content in json. (Dont forget to 1) right click on category and disable private category mode 2 server -> roles -> @everyone -> permissions -> view) +3. Cleaning up: in chosen category delete channels and then create them again. Because it will take years to delete many posts, better to del channels +4. run json_to_txt.py to save data in folders and txt as well +5. pip uninstall discord +