Arabic Election Results Table Extraction from Mednine PDF
This PDF has a data table showing election results from the Tunisian region of Mednine. Challenges include spanning header cells and rotated headers. It has Arabic script.
Updated for testing caching system with cascading dependencies!
I spent far too long making sure Natural PDF supports right-to-left scripts like Arabic. While I can't read them to confirm, I'm vaguely confident that the text we're pulling from the PDF is accurate.
Building a flow
Since this PDF is all one big long table with not much else in the way, we can most likely just stack all of the pages on top of each other with a Flow.
Flows are ways of connecting separate pages or regions vertically or horizontally.
Extracting the table
Since we're using a flow we can just rest easy on .extract_table()
, which automatically combines the tables across the entire flow.
df = flow.extract_table().to_df(header=None)
df
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | الانتخاابت الترشيعية 2014 | None | None | None | None | None | None | None | None | None | None | None |
1 | ورقة كشف نتائج دائرة مدنين | None | None | None | None | None | None | None | None | None | None | None |
2 | مجموع األصوات\nاملصرح بها\nبالدائرة\nاالنتخابية | توزيع األصوات املصرح بها حسب املعتمدية | None | None | None | None | None | None | None | None | القائمات املترشحة | ا\nل\nرت\nت\nي\nب |
3 | None | مدنين\nالشمالية | مدنين\nالجنوبية | سيدي مخلوف | جرجيس | جربة ميدون | جربة حومة\nالسوق | جربة أجيم | بني خداش | بن قردان | None | None |
4 | 426 | 63 | 43 | 56 | 58 | 49 | 55 | 33 | 32 | 37 | قائمة من أجل تحقيق سلطة الشعب | 1 |
5 | 3 001 | 419 | 806 | 197 | 289 | 217 | 616 | 59 | 118 | 280 | قائمة حزب التحالف الديمقراطي | 2 |
6 | 387 | 50 | 60 | 42 | 64 | 28 | 41 | 20 | 42 | 40 | قائمة حزب العدالة والتنمية | 3 |
7 | 1 193 | 135 | 87 | 43 | 170 | 167 | 327 | 39 | 181 | 44 | قائمة الحزب الجمهوري | 4 |
8 | 430 | 117 | 62 | 31 | 43 | 28 | 42 | 23 | 38 | 46 | قائمة حزب الغد | 5 |
9 | 3 066 | 93 | 127 | 123 | 1 422 | 322 | 640 | 68 | 36 | 235 | قائمة حزب آفاق تونس | 6 |
10 | 203 | 14 | 15 | 22 | 33 | 23 | 30 | 6 | 7 | 53 | قائمة حزب االنفتاح والوفاء | 7 |
11 | 192 | 14 | 13 | 14 | 20 | 18 | 21 | 7 | 7 | 78 | قائمة اتحاد الشباب الديمقراطي التونس ي | 8 |
12 | 1 617 | 53 | 59 | 33 | 127 | 282 | 883 | 110 | 25 | 45 | قائمة حزب التيار الديمقراطي | 9 |
13 | 267 | 23 | 32 | 21 | 33 | 25 | 18 | 15 | 21 | 79 | قائمة حزب األمان | 10 |
14 | 4 686 | 254 | 505 | 182 | 2 535 | 221 | 278 | 58 | 91 | 562 | قائمة حركة الشعب | 11 |
15 | 1 443 | 63 | 51 | 58 | 731 | 82 | 326 | 42 | 34 | 56 | قائمة االتحاد من أجل تونس | 12 |
16 | 803 | 96 | 96 | 65 | 119 | 78 | 91 | 47 | 73 | 138 | قائمة شباب مدنين | 13 |
17 | 373 | 28 | 24 | 28 | 38 | 37 | 113 | 30 | 25 | 50 | قائمة حزب املجد | 14 |
18 | 68 795 | 9 010 | 10 898 | 4 013 | 8 627 | 7 912 | 9 854 | 3 358 | 5 021 | 10 102 | قائمة حزب حركة النهضة | 15 |
19 | 309 | 29 | 25 | 17 | 39 | 41 | 41 | 54 | 21 | 42 | قائمة تونس أقوى | 16 |
20 | 204 | 23 | 40 | 9 | 19 | 20 | 22 | 2 | 13 | 56 | قائمة الشعب يريد | 17 |
21 | 119 | 4 | 7 | 6 | 15 | 16 | 44 | 7 | 9 | 11 | قائمة الربيع العربي لتحقيق السيادة\nاالقتصادية | 18 |
22 | 385 | 86 | 51 | 31 | 61 | 33 | 30 | 14 | 32 | 47 | قائمة حزب صوت شعب تونس | 19 |
23 | 576 | 129 | 130 | 54 | 36 | 58 | 87 | 8 | 24 | 50 | قائمة الوحدة الوطنية املستقلة | 20 |
24 | 14 334 | 1 096 | 1 219 | 950 | 3 186 | 1 738 | 3 477 | 492 | 608 | 1 568 | قائمة حركة نداء تونس | 21 |
25 | 2 788 | 101 | 118 | 30 | 877 | 400 | 696 | 119 | 48 | 399 | قائمة الجبهة الشعبية | 22 |
26 | 273 | 20 | 24 | 12 | 23 | 24 | 20 | 26 | 106 | 18 | قائمة التآلف الوطني | 23 |
27 | 901 | 62 | 112 | 48 | 88 | 74 | 107 | 25 | 56 | 329 | قائمة حركة وفاء | 24 |
28 | 4 392 | 449 | 382 | 223 | 565 | 749 | 808 | 163 | 177 | 876 | قائمة حزب االتحاد الوطني الحر | 25 |
29 | 244 | 67 | 21 | 9 | 30 | 14 | 19 | 7 | 56 | 21 | قائمة حزب األصالة | 26 |
30 | 249 | 28 | 25 | 50 | 30 | 20 | 24 | 39 | 15 | 18 | قائمة حركة الديمقراطيين االشتراكيين | 27 |
31 | 6 305 | 575 | 1 158 | 217 | 524 | 1 091 | 1 746 | 283 | 244 | 467 | قائمة حزب املؤتمر من أجل الجمهورية | 28 |
32 | 1 736 | 164 | 143 | 138 | 120 | 465 | 238 | 74 | 205 | 192 | قائمة حزب املبادرة الوطنية الدستورية | 29 |
33 | 2 535 | 168 | 636 | 82 | 221 | 43 | 44 | 12 | 33 | 1 296 | قائمة حماة الوطن | 30 |
34 | 196 | 13 | 45 | 17 | 35 | 17 | 9 | 12 | 13 | 35 | قائمة حركة الدستوريون األحرار | 31 |
35 | 852 | 29 | 48 | 17 | 422 | 87 | 105 | 86 | 15 | 43 | قائمة حزب التكتل | 32 |
36 | 1 224 | 129 | 169 | 47 | 160 | 153 | 140 | 19 | 263 | 144 | قائمة تيار املحبة | 33 |
37 | 503 | 17 | 29 | 17 | 22 | 82 | 258 | 21 | 18 | 39 | قائمة حزب البناء الوطني | 34 |
38 | 125 007 | 13 621 | 17 260 | 6 902 | 20 782 | 14 614 | 21 250 | 5 378 | 7 707 | 17 496 | األصوات املصرح بها لكل القائمات | <NA> |
If you'd rather not use a Flow, an alternative is going through each page. Instead of a for
loop I like to use .apply
, as it keeps things a bit shorter. You could also use a list comprehension!
import pandas as pd
dataframes = pdf.pages.apply(
lambda page: page.extract_table().to_df(header=None)
)
print("Found", len(dataframes), "tables")
# Combine
df = pd.concat(dataframes, ignore_index=True)
df
Found 4 tables
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | الانتخاابت الترشيعية 2014 | None | None | None | None | None | None | None | None | None | None | None |
1 | ورقة كشف نتائج دائرة مدنين | None | None | None | None | None | None | None | None | None | None | None |
2 | مجموع األصوات\nاملصرح بها\nبالدائرة\nاالنتخابية | توزيع األصوات املصرح بها حسب املعتمدية | None | None | None | None | None | None | None | None | القائمات املترشحة | ا\nل\nرت\nت\nي\nب |
3 | None | مدنين\nالشمالية | مدنين\nالجنوبية | سيدي مخلوف | جرجيس | جربة ميدون | جربة حومة\nالسوق | جربة أجيم | بني خداش | بن قردان | None | None |
4 | 426 | 63 | 43 | 56 | 58 | 49 | 55 | 33 | 32 | 37 | قائمة من أجل تحقيق سلطة الشعب | 1 |
5 | 3 001 | 419 | 806 | 197 | 289 | 217 | 616 | 59 | 118 | 280 | قائمة حزب التحالف الديمقراطي | 2 |
6 | 387 | 50 | 60 | 42 | 64 | 28 | 41 | 20 | 42 | 40 | قائمة حزب العدالة والتنمية | 3 |
7 | 1 193 | 135 | 87 | 43 | 170 | 167 | 327 | 39 | 181 | 44 | قائمة الحزب الجمهوري | 4 |
8 | 430 | 117 | 62 | 31 | 43 | 28 | 42 | 23 | 38 | 46 | قائمة حزب الغد | 5 |
9 | 3 066 | 93 | 127 | 123 | 1 422 | 322 | 640 | 68 | 36 | 235 | قائمة حزب آفاق تونس | 6 |
10 | 203 | 14 | 15 | 22 | 33 | 23 | 30 | 6 | 7 | 53 | قائمة حزب االنفتاح والوفاء | 7 |
11 | 192 | 14 | 13 | 14 | 20 | 18 | 21 | 7 | 7 | 78 | قائمة اتحاد الشباب الديمقراطي التونس ي | 8 |
12 | 1 617 | 53 | 59 | 33 | 127 | 282 | 883 | 110 | 25 | 45 | قائمة حزب التيار الديمقراطي | 9 |
13 | 267 | 23 | 32 | 21 | 33 | 25 | 18 | 15 | 21 | 79 | قائمة حزب األمان | 10 |
14 | 4 686 | 254 | 505 | 182 | 2 535 | 221 | 278 | 58 | 91 | 562 | قائمة حركة الشعب | 11 |
15 | 1 443 | 63 | 51 | 58 | 731 | 82 | 326 | 42 | 34 | 56 | قائمة االتحاد من أجل تونس | 12 |
16 | 803 | 96 | 96 | 65 | 119 | 78 | 91 | 47 | 73 | 138 | قائمة شباب مدنين | 13 |
17 | 373 | 28 | 24 | 28 | 38 | 37 | 113 | 30 | 25 | 50 | قائمة حزب املجد | 14 |
18 | 68 795 | 9 010 | 10 898 | 4 013 | 8 627 | 7 912 | 9 854 | 3 358 | 5 021 | 10 102 | قائمة حزب حركة النهضة | 15 |
19 | 309 | 29 | 25 | 17 | 39 | 41 | 41 | 54 | 21 | 42 | قائمة تونس أقوى | 16 |
20 | 204 | 23 | 40 | 9 | 19 | 20 | 22 | 2 | 13 | 56 | قائمة الشعب يريد | 17 |
21 | 119 | 4 | 7 | 6 | 15 | 16 | 44 | 7 | 9 | 11 | قائمة الربيع العربي لتحقيق السيادة\nاالقتصادية | 18 |
22 | 385 | 86 | 51 | 31 | 61 | 33 | 30 | 14 | 32 | 47 | قائمة حزب صوت شعب تونس | 19 |
23 | 576 | 129 | 130 | 54 | 36 | 58 | 87 | 8 | 24 | 50 | قائمة الوحدة الوطنية املستقلة | 20 |
24 | 14 334 | 1 096 | 1 219 | 950 | 3 186 | 1 738 | 3 477 | 492 | 608 | 1 568 | قائمة حركة نداء تونس | 21 |
25 | 2 788 | 101 | 118 | 30 | 877 | 400 | 696 | 119 | 48 | 399 | قائمة الجبهة الشعبية | 22 |
26 | 273 | 20 | 24 | 12 | 23 | 24 | 20 | 26 | 106 | 18 | قائمة التآلف الوطني | 23 |
27 | 901 | 62 | 112 | 48 | 88 | 74 | 107 | 25 | 56 | 329 | قائمة حركة وفاء | 24 |
28 | 4 392 | 449 | 382 | 223 | 565 | 749 | 808 | 163 | 177 | 876 | قائمة حزب االتحاد الوطني الحر | 25 |
29 | 244 | 67 | 21 | 9 | 30 | 14 | 19 | 7 | 56 | 21 | قائمة حزب األصالة | 26 |
30 | 249 | 28 | 25 | 50 | 30 | 20 | 24 | 39 | 15 | 18 | قائمة حركة الديمقراطيين االشتراكيين | 27 |
31 | 6 305 | 575 | 1 158 | 217 | 524 | 1 091 | 1 746 | 283 | 244 | 467 | قائمة حزب املؤتمر من أجل الجمهورية | 28 |
32 | 1 736 | 164 | 143 | 138 | 120 | 465 | 238 | 74 | 205 | 192 | قائمة حزب املبادرة الوطنية الدستورية | 29 |
33 | 2 535 | 168 | 636 | 82 | 221 | 43 | 44 | 12 | 33 | 1 296 | قائمة حماة الوطن | 30 |
34 | 196 | 13 | 45 | 17 | 35 | 17 | 9 | 12 | 13 | 35 | قائمة حركة الدستوريون األحرار | 31 |
35 | 852 | 29 | 48 | 17 | 422 | 87 | 105 | 86 | 15 | 43 | قائمة حزب التكتل | 32 |
36 | 1 224 | 129 | 169 | 47 | 160 | 153 | 140 | 19 | 263 | 144 | قائمة تيار املحبة | 33 |
37 | 503 | 17 | 29 | 17 | 22 | 82 | 258 | 21 | 18 | 39 | قائمة حزب البناء الوطني | 34 |
38 | 125 007 | 13 621 | 17 260 | 6 902 | 20 782 | 14 614 | 21 250 | 5 378 | 7 707 | 17 496 | األصوات املصرح بها لكل القائمات | <NA> |
I'm a big fan of taking as much information as possible, then cleaning it up later. We could spend time wrangling the column headers on the first page, spacing out grids, etc etc etc, but instead let's just grab the whole thing and sort it out later.
Cleaning up the data
Now it just becomes an exercise in data cleanup! This is something AI coding tools are excellent at, so feel free to lean hard on them.
# Use row 2 as header
df.columns = df.iloc[3].fillna(df.iloc[2]).str.replace("\n", " ")
# Drop the first 3 rows
df = df.iloc[4:].reset_index(drop=True)
# Remove spaces from numbers and convert to int
numeric_cols = df.columns[0:4]
df[numeric_cols] = df[numeric_cols].replace(r"\s+", "", regex=True).astype(int)
df
3 | مجموع األصوات املصرح بها بالدائرة االنتخابية | مدنين الشمالية | مدنين الجنوبية | سيدي مخلوف | جرجيس | جربة ميدون | جربة حومة السوق | جربة أجيم | بني خداش | بن قردان | القائمات املترشحة | ا ل رت ت ي ب |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 426 | 63 | 43 | 56 | 58 | 49 | 55 | 33 | 32 | 37 | قائمة من أجل تحقيق سلطة الشعب | 1 |
1 | 3001 | 419 | 806 | 197 | 289 | 217 | 616 | 59 | 118 | 280 | قائمة حزب التحالف الديمقراطي | 2 |
2 | 387 | 50 | 60 | 42 | 64 | 28 | 41 | 20 | 42 | 40 | قائمة حزب العدالة والتنمية | 3 |
3 | 1193 | 135 | 87 | 43 | 170 | 167 | 327 | 39 | 181 | 44 | قائمة الحزب الجمهوري | 4 |
4 | 430 | 117 | 62 | 31 | 43 | 28 | 42 | 23 | 38 | 46 | قائمة حزب الغد | 5 |
5 | 3066 | 93 | 127 | 123 | 1 422 | 322 | 640 | 68 | 36 | 235 | قائمة حزب آفاق تونس | 6 |
6 | 203 | 14 | 15 | 22 | 33 | 23 | 30 | 6 | 7 | 53 | قائمة حزب االنفتاح والوفاء | 7 |
7 | 192 | 14 | 13 | 14 | 20 | 18 | 21 | 7 | 7 | 78 | قائمة اتحاد الشباب الديمقراطي التونس ي | 8 |
8 | 1617 | 53 | 59 | 33 | 127 | 282 | 883 | 110 | 25 | 45 | قائمة حزب التيار الديمقراطي | 9 |
9 | 267 | 23 | 32 | 21 | 33 | 25 | 18 | 15 | 21 | 79 | قائمة حزب األمان | 10 |
10 | 4686 | 254 | 505 | 182 | 2 535 | 221 | 278 | 58 | 91 | 562 | قائمة حركة الشعب | 11 |
11 | 1443 | 63 | 51 | 58 | 731 | 82 | 326 | 42 | 34 | 56 | قائمة االتحاد من أجل تونس | 12 |
12 | 803 | 96 | 96 | 65 | 119 | 78 | 91 | 47 | 73 | 138 | قائمة شباب مدنين | 13 |
13 | 373 | 28 | 24 | 28 | 38 | 37 | 113 | 30 | 25 | 50 | قائمة حزب املجد | 14 |
14 | 68795 | 9010 | 10898 | 4013 | 8 627 | 7 912 | 9 854 | 3 358 | 5 021 | 10 102 | قائمة حزب حركة النهضة | 15 |
15 | 309 | 29 | 25 | 17 | 39 | 41 | 41 | 54 | 21 | 42 | قائمة تونس أقوى | 16 |
16 | 204 | 23 | 40 | 9 | 19 | 20 | 22 | 2 | 13 | 56 | قائمة الشعب يريد | 17 |
17 | 119 | 4 | 7 | 6 | 15 | 16 | 44 | 7 | 9 | 11 | قائمة الربيع العربي لتحقيق السيادة\nاالقتصادية | 18 |
18 | 385 | 86 | 51 | 31 | 61 | 33 | 30 | 14 | 32 | 47 | قائمة حزب صوت شعب تونس | 19 |
19 | 576 | 129 | 130 | 54 | 36 | 58 | 87 | 8 | 24 | 50 | قائمة الوحدة الوطنية املستقلة | 20 |
20 | 14334 | 1096 | 1219 | 950 | 3 186 | 1 738 | 3 477 | 492 | 608 | 1 568 | قائمة حركة نداء تونس | 21 |
21 | 2788 | 101 | 118 | 30 | 877 | 400 | 696 | 119 | 48 | 399 | قائمة الجبهة الشعبية | 22 |
22 | 273 | 20 | 24 | 12 | 23 | 24 | 20 | 26 | 106 | 18 | قائمة التآلف الوطني | 23 |
23 | 901 | 62 | 112 | 48 | 88 | 74 | 107 | 25 | 56 | 329 | قائمة حركة وفاء | 24 |
24 | 4392 | 449 | 382 | 223 | 565 | 749 | 808 | 163 | 177 | 876 | قائمة حزب االتحاد الوطني الحر | 25 |
25 | 244 | 67 | 21 | 9 | 30 | 14 | 19 | 7 | 56 | 21 | قائمة حزب األصالة | 26 |
26 | 249 | 28 | 25 | 50 | 30 | 20 | 24 | 39 | 15 | 18 | قائمة حركة الديمقراطيين االشتراكيين | 27 |
27 | 6305 | 575 | 1158 | 217 | 524 | 1 091 | 1 746 | 283 | 244 | 467 | قائمة حزب املؤتمر من أجل الجمهورية | 28 |
28 | 1736 | 164 | 143 | 138 | 120 | 465 | 238 | 74 | 205 | 192 | قائمة حزب املبادرة الوطنية الدستورية | 29 |
29 | 2535 | 168 | 636 | 82 | 221 | 43 | 44 | 12 | 33 | 1 296 | قائمة حماة الوطن | 30 |
30 | 196 | 13 | 45 | 17 | 35 | 17 | 9 | 12 | 13 | 35 | قائمة حركة الدستوريون األحرار | 31 |
31 | 852 | 29 | 48 | 17 | 422 | 87 | 105 | 86 | 15 | 43 | قائمة حزب التكتل | 32 |
32 | 1224 | 129 | 169 | 47 | 160 | 153 | 140 | 19 | 263 | 144 | قائمة تيار املحبة | 33 |
33 | 503 | 17 | 29 | 17 | 22 | 82 | 258 | 21 | 18 | 39 | قائمة حزب البناء الوطني | 34 |
34 | 125007 | 13621 | 17260 | 6902 | 20 782 | 14 614 | 21 250 | 5 378 | 7 707 | 17 496 | األصوات املصرح بها لكل القائمات | <NA> |