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!

from natural_pdf import PDF

pdf = PDF("mednine.pdf")
pdf.show(cols=3)

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.

Multiple approaches

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.

from natural_pdf.flows import Flow

flow = Flow(pdf.pages, arrangement='vertical')
flow.show(width=300)

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
Output
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>