Po latach wykonałem analizę (z rozpędu, zmotywowany analizowaniem wyników Danuty Hojarskiej). W województwie pomorskim są dwa okręgi wyborcze: 25 (Gdańsk) oraz 26 Gdynia. Zbiorcze wyniki z protokołów, które w swoim czasie pobrałem ze strony PKW są następujące:
Komisje obwodowe wg ROZKŁADU głosów ważnych ** Okręg 26 126707 ważne głosy ** Kandydat Komitet Głosy Komisje Max % Średnia Me ------------------------------------------------------------- Gromadzki Stonoga 1521 795 12 1.20 1,9 1.0 Furgo Petru 14239 795 224 11.23 17,9 10.00 Zwiercan Kukiz 11801 795 65 9.31 14.8 13.0 Miller Zlew 11524 795 72 9.09 14.5 10.0 Lewna Psl 4644 795 213 3.66 5.8 2.0 Wysocki Korwin 9491 795 54 7.49 11.9 10.0 Biernacki Po 42535 795 312 33.56 53.5 38.0 Szczypińska PiS 30952 795 150 24.42 38.9 35.0 ** Okręg 25 130348 ważne głosy ** Kandydat Komitet Głosy Komisje Max % Średnia Me ------------------------------------------------------------- Hojarska Stonoga 1668 656 152 1.27 2.5 1.0 Lieder Petru 23155 656 256 35.76 35.3 22.0 Błeńska Kukiz 9410 656 45 7.21 14.3 14.0 Senyszyn Zlew 13143 656 75 10.08 20.3 17.0 Sarnowski Psl 3457 656 134 2.65 5.3 2.0 Rabenda Korwin 8903 656 65 6.83 13.6 11.0 Krotowska Razem 6995 656 45 5.36 10.7 9.0 Korol PiS 28657 656 252 21.98 43.7 30.0 Sellin PiS 34960 656 213 26.82 53.3 40.0
Te wyniki mogą (ale nie muszą) się ciut-niewiele różnić od oficjalnych--nie porównywałem
Czyli na przykład pani Hojarska dostała 1668 głosów w 656 komisjach co daje średnio 2,5 głosa/komisję (Mediana 1 głos). Rozkłady liczby głosów przedstawione w postaci wykresów słupkowych wyglądają następująco (pierwsze trzy rysunki to okręg 25, następne trzy to okręg 26):
No i wreszcie przedstawienie wyników na mapie z wykorzystanie Google Fusion Tables link do GFT:
Link do danych jest tutaj.
Uczono mnie, że współczynnik skośności aczkolwiek może przyjąć wartości większe od 3, to w praktyce taka sytuacja się nie zdarza. Dlatego z rezerwą podszedłem do obliczeń, z których wynikało, że dla pewnego zbioru danych wynosi on 14:
library(moments) # Wynik 1-ki z listy komitetu Z.Stonogi (D.Hojarska, wybory 2015) s <- read.csv("hojarska.csv", sep = ';', header=T, na.string="NA"); skewness(s$glosy) [1] 14.08602 nrow(s) [1] 657 sum(s$glosy) [1] 1671
Pierwsza hipoteza jest taka, że formuła liczenia współczynnika może być egzotyczna. Ustalmy zatem jak toto jest liczone:
?skewness Description: This function computes skewness of given data
Wiele to się nie dowiedziałem. Po ściągnięciu źródła można ustalić, że to współczynnik klasyczny czyli iloraz trzeciego momentu centralnego przez odchylenie standardowe do trzeciej potęgi. Zatem jak najbardziej klasyczny a nie egzotyczny. Sprawdźmy co wyliczy Perl dla pewności:
#!/usr/bin/perl print STDERR "** Użycie: $0 plik numer-kolumny (pierwszy wiersz jest pomijany)\n"; print STDERR "** Domyślnie: wyniki kandydata nr1 na liście komitetu Z.Stonoga (okr.25 / D.Hojarska)\n"; $file = "hojarska.csv"; ## default file $column = 1; ## first column = 0 if ( $#ARGV >= 0 ) { $file=$ARGV[0]; } if ( $#ARGV >= 1 ) { $column=$ARGV[1]; } open (S, "$file") || die "Cannot open $file\n"; print "\nDane z pliku $file (kolumna: $column):\n"; $hdr = <S> ; # wczytaj i pomin naglowek ($n będzie prawidłowe) while (<S>) { chomp(); @tmp = split (/;/, $_); $v = $tmp[$column]; push(@L, $v) ; $sum += $v; $Counts{"$v"}++; $n++; } # Wyznaczenie średniej: $mean = $sum /$n; ## Wyznaczenie dominanty: ## przy okazji wydrukowanie danych pogrupowanych print "+--------+---------+--------+--------+--------+\n"; print "| Głosy | Obwody | cumGł | cumGł% | cumN% |\n"; print "+--------+---------+--------+--------+--------+\n"; $maxc = -1; for $c (sort {$a <=> $b } keys %Counts ) { $sumCum += $Counts{"$c"} * $c; $nCum += $Counts{"$c"}; if ($maxc_pos < $Counts{$c} ) { $maxc_pos = $Counts{$c} ; $maxc_val = $c } printf "| %6d | %6d | %6d | %6.2f | %6.2f |\n", $c, $Counts{$c}, $sumCum, $sumCum/$sum *100, $nCum/$n *100; } print "+--------+---------+--------+--------+--------+\n\n"; $mode = $maxc_val; # dominanta ## Wyznaczenie mediany: $half = int(($#L +1)/2 ); @L = sort ({ $a <=> $b } @L) ; #numerycznie ##print "$half of @L\n"; $median = $L[$half]; printf "Średnie: x̄ = %.3f (N=%d) Me =%.3f D = %.3f\n", $mean, $n, $median, $mode; ## Odchylenia od średniej: for my $l (@L) {## $sd1 = ($l - $mean) ; $sd2 = $sd1 * $sd1; # ^2 $sd3 = $sd2 * $sd1; # ^3 $sum_sd1 += $sd1; $sum_sd2 += $sd2; $sum_sd3 += $sd3; } # odchylenie std./3-ci moment: $sd = sqrt(($sum_sd2 / $n)); $u3 = $sum_sd3/$n; printf "Sumy (x-x̄): %.2f(¹) %.2f(²) %.2f(³)\n", $sum_sd1, $sum_sd2, $sum_sd3; printf "Rozproszenie: σ = %.3f µ³ = %.3f\n", $sd, $u3; printf "Skośność: (x̄-D)/σ = %.2f", ($mean -$mode)/$sd; printf " 3(x̄-Me)/σ) = %.2f", 3 * ($mean - $median)/$sd; printf " (µ³/σ³) = %.2f\n", $u3/($sd*$sd*$sd); ##//
Uruchomienie powyższego skryptu daje w wyniku to samo. Moja pewność, że wszystko jest OK jest teraz (prawie że) stuprocentowa.
Wracając do R:
library(ggplot2); p <- ggplot(data = s, aes(x = glosy)) + geom_histogram(binwidth = 4) p breaks <- ggplot_build(p)$data breaks [[1]] y count x xmin xmax density ncount ndensity PANEL group 1 482 482 0 -2 2 0.1834094368 1.000000000 2628.000000 1 -1 2 140 140 4 2 6 0.0532724505 0.290456432 763.319502 1 -1 ...
Pierwszy przedział jest definiowany jako (-2;2], ale z uwagi na wartość danych de facto liczy głosy w komisjach, w których Hojarska dostała 0--2 głosów (drugi to (2;6], itd) i ni cholery nie da się tego zmienić na coś bardziej zbliżonego do prawdy. Bo tak jak jest to faktycznie pierwszy przedział jest dwa razy węższy niż każdy pozostały. W szczególności prawdziwa średnia, w tym przedziale wynosi 1,259 głosa, zaś liczona jako środek przedziału przez liczebność wynosi 1.0 (482 *1/482). Wg formuły ggplota środek przedziału to zero zatem średnia też wyjdzie 0, tj. wartość jest wyznaczona z dużym/nieokreślonym błędem.
Dzieje się tak ponieważ: the histogram is centered at 0, and the first bars xlimits are at 0.5*binwidth and -0.5*binwidth. Dopiero gdy dane są dodatnie, to ggplot zaczyna od zera. No ale nasz zbiór zawiera zera i klops. Zmiana tego jest nietrywialna.
Zamiast ggplota moża użyć po prostu polecenia hist:
h <- hist(s$glosy, breaks=seq(0,max(s$glosy), by=4) ) h $breaks [1] 0 4 8 12 16 20 24 28 32 36 40 44 48 52 56 60 64 68 72 [20] 76 80 84 88 92 96 100 104 108 112 116 120 124 128 132 136 140 144 148 [39] 152 $counts [1] 592 42 6 4 2 2 2 1 2 1 2 0 0 0 0 0 0 0 0 [20] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 $density [1] 0.2252663623 0.0159817352 0.0022831050 0.0015220700 0.0007610350 [6] 0.0007610350 0.0007610350 0.0003805175 0.0007610350 0.0003805175 [11] 0.0007610350 0.0000000000 0.0000000000 0.0000000000 0.0000000000 [16] 0.0000000000 0.0000000000 0.0000000000 0.0000000000 0.0000000000 [21] 0.0000000000 0.0000000000 0.0000000000 0.0000000000 0.0000000000 [26] 0.0000000000 0.0000000000 0.0000000000 0.0000000000 0.0000000000 [31] 0.0000000000 0.0000000000 0.0000000000 0.0000000000 0.0000000000 [36] 0.0000000000 0.0000000000 0.0003805175 $mids [1] 2 6 10 14 18 22 26 30 34 38 42 46 50 54 58 62 66 70 74 [20] 78 82 86 90 94 98 102 106 110 114 118 122 126 130 134 138 142 146 150
No teraz liczy (prawie) jak trzeba pierwszy przedział [0;4], drugi (4;8] itd. Prawie bo na upartego pierwszy przedział jest szeroki na 5 wartości a kolejne na 4. Eh te detale i te upierdliwe zero:-)
Przy okazji się zadumałem, a cóż to jest to density. W pierwszym podejściu, że to są częstości, ale nie, bo ewidentnie nie sumują się do 1:
?hist density: values f^(x[i]), as estimated density values. If `all(diff(breaks) == 1)', they are the relative frequencies `counts/n' and in general satisfy sum[i; f^(x[i]) (b[i+1]-b[i])] = 1, where b[i] = `breaks[i]
No dość kryptycznie, ale można się domyśleć, że nie $\sum_i d_i =1$, ale $\sum_i d_i * w_i$, gdzie $w_i$, to szerokość przedziału $i$ (a $d_i$ oznacza gęstość w przedziale $i$ oczywiście). Pole pod krzywą będzie zatem równe 1, jak tego należy się spodziewać po gęstości.
Wprawdzie już to padło mimochodem wyżej, ale analizowany zbiór danych to liczba oddanych głosów na numer jeden na liście komitetu wyborczego Zbigniewa Stonogi w okręgu wyborczym 25 w wyborach do Sejmu w 2015 roku. Tym numerem jeden była Danuta Hojarska, która faktycznie dostała 152 głosy w jednej komisji (we wsi, w której mieszka) oraz 0--4 głosów w 90% pozostałych komisjach (w tym 0 głosów w 191/657.0 = 29% komisjach).
Ze starych zapasów odgrzebałem plik CSV ze współrzędnymi obwodowych komisji wyborczych i połączyłem go z wynikami uzyskanymi przez Hojarską z zamiarem wyświetlenia tego na mapie za pomocą Google Fusion Tables (GFT):
obwod;coordexact;glosy;icon 101884;54.2701205 18.6317438;3;measle_white 101885;54.260714 18.6282809;0;measle_white 101886;54.262187 18.632082;2;measle_white 101887;54.257501 18.63147;1;measle_white 101888;54.25786 18.6306574;7;measle_grey ...
Kolumna icon
zawiera nazwę ikony, która jest
definiowana wg schematu: 0--3 głosy biala (measle_white); 4--9 głosów
szara (measle_grey) 9-19 głosów żółta (small_yellow) 20--99 głosów
czerwona (small_red) 100 i więcej głosów purpurowa (small_purple).
Zestawienie dostępnych w GFT ikon można znaleźć
tutaj.
Konfigurowanie GFT aby korzystała z kolumny z nazwami ikon
sprowadza się do wyklikania stosownej pozycji
(Configure map →Feature map →Change feature styles
→Use icon specified in a column.)
Przy okazji odkurzyłem zasób skryptów/zasobów używanych
do geokodowania i/lub przygotowywania danych pod potrzeby GFT,
w szczególności:
joincvs.pl
(łączy dwa pliki csv w oparciu o wartości
n-tej kolumny w pierwszym pliku oraz m-tej kolumny w drugim);
geocodeCoder0.pl
(uruchamia geokodera Google).
Oba skrypty i jeszcze parę rzeczy można znaleźć:
tutaj.
UWAGA: Ten tekst nie jest o polityce ale o [elementarnej] statystyce.
Media informowały, że posłowie PiS Adam Hofman, Mariusz A. Kamiński i Adam Rogacki wzięli na podróż do Madrytu na posiedzenie komisji Zgromadzenia Parlamentarnego Rady Europy po kilkanaście tysięcy złotych zaliczki, zgłaszając wyjazd samochodem; w rzeczywistości polecieli tanimi liniami lotniczymi. Ponieważ kontrola wydatków posłów jest iluzoryczna różnica pomiędzy kosztem podróży samochodem a samolotem [za dużo mniejsze pieniądze] miała stanowić dodatkowy przychód wyżej wymienionych. Według prokuratury, która wszczęła śledztwo, zachodzi podejrzenie popełnienia oszustwa.
Łapiąc wiatr w żagle [sprawa się upubliczniła tuż przed ostatnimi wyborami samorządowymi] koalicja rządząca w osobie Marszałka Sejmu RP Sikorskiego zarządziła audyt, którego efektem było udostępnienie m.in. dokumentu pn. Wyjazdy zagraniczne posłów VII kadencja (kopia jest tutaj).
Jak przystało na kraj, w którym od lat działa Ministerstwo cyfryzacji zestawienie jest w formacie PDF, zatem pierwszym ruchem była zamiana na coś przetwarzalnego. Wpisanie w google PDF+Excel+conversion skutkuje ogromną listą potencjalnych konwerterów. Bagatelizując skalę problemu spróbowałem dokonać konwersji narzędziami dostępnymi on-line, ale z marnym rezultatem (za duży dokument przykładowo; serwis za free jest tylko dla PDFów mniejszych niż 50 stron). W przypadku Convert PDF to EXCEL online & free coś tam skonwertował, nawet wyglądało toto na pierwszy rzut oka OK ale na drugi już nie: dokument niekompletny oraz nieprawidłowo zamienione niektóre liczby (przykładowo zamiast 837,50 zł w arkuszu jest 83750 -- 100 razy więcej!).
Ostatecznie skończyło się na ściągnięciu 30 dniowej wersji Adobe Acrobata Pro XI, który faktycznie sprawdził się w roli konwertera PDF→XLSX. Do konwersji wykorzystałem służbowego laptopa Elki wyposażonego w legalny Office 2010, na którym zainstalowałem ww. AA Pro XI. OOffice niby czyta XLSX, ale z koszmarnymi błędami, więc żeby dalej móc obrabiać arkusz w Linuksie wczytałem wynikowy XLSX do Excela 2010 po czym zapisałem go w (starszym) formacie XLS. Ten plik wyświetlił się w OO Calcu bez problemu.
Arkusz jest tak sformatowany, że 4 pierwsze komórki oraz są często wielowierszowe i scalone, zawierają bowiem liczbę porządkową, datę, miejsce i cel wyjazdu delegacji posłów. Po zamianie na plik CSV zawartość komórek scalonych pojawi się w pierwszym wierszu, a pozostałe będą puste. Prostym skryptem Perlowym mogę wypełnić puste komórki wg. algorytmu: jeżeli cztery pierwsze pola są puste, to skopiuj wartości ostatnich niepustych:
if ($tmp[0] eq '' && $tmp[1] eq '' && $tmp[2] eq '' && $tmp[3] eq '' ) { ... }
Pierwszy problem: wielowierszowe komórki z kolumn 1--4 nie zawsze są scalone. Czasem tekst jest podzielony na wiersze co psuje konwersję. Ręcznie scalam niescalone komórki (trochę to trwa). Przed scaleniem usuwam z kolumn 1--4 końce wiersza.
Drugi problem: część liczb nie jest liczbami z uwagi na użycie separatora tysięcy, który się zamienił w PDFie na odstęp (spację). Zatem zaznaczam kolumny zawierające różne pozycje kosztów po czym:
Po uporządkowaniu arkusza, zapisuję go w formacie CSV. Następnie prostym skryptem Perlowym zamieniam na taki plik CSV, w którym puste komórki są wypełniane zawartością z poprzednich wierszy. Kolumna Państwo - miasto jest kopiowana. Kopia jest zmieniana na jednoznaczne: Państwo, miasto (pierwszy-kraj, przecinek, pierwsze miasto z listy celów podróży -- żeby geokoderowi było łatwiej.)
Innym skryptem Perlowym dodaję do pliku CSV 3 kolumny, które zawierają:
współrzędne celu podróży (w tym celu zamieniam adres Państwo, miasto na współrzędne geograficzne korzystając z geokodera Google);
odległość w kilometrach pomiędzy punktem o współrzędnych
21.028075/52.225208 (W-wa, Wiejska 1) a celem podróży (obliczoną przy wykorzystaniu pakietu
GIS::Distance
);
linię zdefiniowana w formacie KML o końcach 21.028075/52.225208--współrzędne-celu-podróży (do ewentualnego wykorzystania z Google Fusion Tables).
#!/usr/bin/perl # use Storable; use Google::GeoCoder::Smart; use GIS::Distance; $geo = Google::GeoCoder::Smart->new(); my $gis = GIS::Distance->new(); my $GeoCodeCacheName = 'geocode.cache'; my $NewCoordinatesFetched=0; # global flag my $SLEEP_TIME = 2 ; my $coords_okr = "21.028075,52.225208"; # Warszawa = środek świata my %GeoCodeCache = %{ retrieve("$GeoCodeCacheName") } if ( -f "$GeoCodeCacheName" ) ; my ($wwa_lng, $wwa_lat) = split (",", $coords_okr); my $linesNo = 0 ; my $GCtotaluse = 1; # laczna liczba wywolan geocodera while (<>) { $linesNo++; chomp(); $_ =~ s/[ \t]+;[ \t]+/;/g; ## usuń ew. niepotrzebne spacje @line = split ";", $_; print STDERR "**$linesNo = $line[3] ... "; # geokodowanie (uwaga na limit) # Poprawki dla miejsc, których nie zna Google: $line[3] =~ s/Erewań/Erywań/; ## $line[3] =~ s/Sowayma/Madaba/; ## najbliższe miasto $line[3] =~ s/Bołszowce/Iwano-Frankiwsk/; ## najbliższe miasto my $coords = addr2coords( $line[3] ); ($tmp_lat, $tmp_lng, $gcuse) = split " ", $coords; if ($gcuse > 0) {$GCtotaluse++ ; } $distance = $gis->distance($tmp_lat,$tmp_lng => $wwa_lat,$wwa_lng ); $distance_t = sprintf ("%.1f", $distance); my $kml_line = "<LineString><coordinates>$tmp_lng,$tmp_lat $coords_okr</coordinates></LineString>"; print "$_;\"$coords\";$distance_t;\"$kml_line\"\n"; print STDERR "\n"; if ($GCtotaluse % 100 == 0 ) {# store every 100 geocoder calls store(\%GeoCodeCache, "$GeoCodeCacheName"); print STDERR "\n... Cache stored. ***\n"; } } ## store(\%GeoCodeCache, "$GeoCodeCacheName"); ## ## ## #### sub addr2coords { my $a = shift ; my $r = shift || 'n'; my ($lat, $lng) ; my $GCuse = 0; ##consult cache first if (exists $GeoCodeCache{"$a"} ) { print STDERR "Coordinates catched ... $a "; ($lat,$lng) = split (" ", $GeoCodeCache{"$a"} ); } else { print STDERR "Geocoding ... $a "; my ($resultnum, $error, @results, $returncontent) = $geo->geocode("address" => "$a"); $GCuse = 1; sleep $SLEEP_TIME; ## make short pause $resultnum--; $resultNo=$resultnum ; if (resultNo > 0) { print STDERR "** Location $a occured more than once! **" } if ($error eq 'OK') { $NewCoordinatesFetched=1; for $num(0 .. $resultnum) { $lat = $results[$num]{geometry}{location}{lat}; $lng = $results[$num]{geometry}{location}{lng}; ##print "*** LAT/LNG:$lat $lng ERROR: $error RES: $resultNo ***\n"; } $GeoCodeCache{"$a"} = "$lat $lng"; ## store in cache } else { print STDERR "** Location $a not found! due to $error **" } } if ($r eq 'r' ) { return "$lng,$lat,$GCuse"; } # w formacie KML else { return "$lat $lng $GCuse"; } }
Gotowy plik CSV zawierający zestawienie podróży jest dostępny tutaj.
Na podstawie zestawienia i z użyciem pakietu ggplot2 generują się takie oto śliczne wykresy.
Wszystkie podróże z zestawienie (N=1874; odpowiednio: koszt łączny, koszt transportu, długość w tys km):
Tylko podróże dla których koszt transportu był niezerowy (N=1423; odpowiednio: koszt łączny, koszt transportu, długość w tys km):
Poniższy skrypt R sumuje i drukuje wszystkie podróże każdego posła:
require(plyr) d <- read.csv("W7RR_podroze_by_podroz1.csv", sep = ';', dec = ",", header=T, na.string="NA"); # Dodaj kolumnę której wartości to konkatenacja: "Poseł|Klub" d[,"PosKlub"] <- do.call(paste, c(d[c("Posel", "Klub")], sep = "|")); # Usuń wszystko za wyjątkiem tego co potrzeba: d <- d[ c("PosKlub", "Klacznie", "Ktransp", "Dist") ]; # Sumowanie po PosKlub PSums <- as.data.frame ( ddply(d, .(PosKlub), numcolwise(sum)) ); # Z powrotem rozdziel kolumnę "Poseł|Klub" na dwie PSums <- as.data.frame ( within(PSums, PosKlub <-data.frame( do.call('rbind', strsplit(as.character(PosKlub), '|', fixed=TRUE)))) ) # Drukuj PSums;
Z pliku .Rout
kopiuję zestawienie łącznych wydatków
posłów oraz łącznej pokonanej przez nich odległości:
PosKlub.X1 PosKlub.X2 KlacznieT KtranspT DistT 1 Adam Abramowicz PiS 4.02599 2.64595 1.3153 2 Adam Hofman PiS 119.55271 59.53315 26.1716 3 Adam Kępiński SLD 10.15754 7.93882 3.8069 4 Adam Kępiński TR 12.63098 8.02327 2.2107 ...
Uwaga: kilkanaście nazwisk się powtarza ponieważ posłowie zmienili przynależność klubową w czasie trwania kadencji [Aby uwzględnić takich posłów sumowanie odbywało się po wartościach zmiennej zawierającej połączone napisy Poseł|Klub.]
Na podstawie takiego z kolei zestawienia i znowu z użyciem ggplot2 generują inne śliczne wykresy.
Uwaga: sumowane tylko podróże, dla których koszt transportu był niezerowy (N=1423; odpowiednio: koszt łączny, koszt transportu, długość w tys km):
Link do tabeli zawierającej zestawienie podróży w formacie Google Fusion Tables jest tutaj.
Dane + skrypty dostępne są także w: github.com/hrpunio/Data.
Google fusion tables another excercise.
Two data sets describe football players who plays in Polish t-Mobile ekstraklasa (1st division) and Pierwsza Liga (2nd division) in 2011/2012 (autumn).
To show from where the player came a straight line is drawn from a player's birthplace to club's stadium, the player plays for.
Figure 1. 1st division.
Figure 2. 2nd division.
Players from 2nd division seems to be born closer to the clubs they play for:-)
Warning: in considerable number of cases the geocoding as performed by Google maybe wrong due to poor data quality--have no time to check/correct.
Yet another excercise, which tests Google Fusion Tables.
The data set contains--among other things--birth place for 600+ rugby players who last year take part in Rugby World Cup in New Zealand. The raw data is available here and here.
On the following diagram (cf. Figure 1) the players are mapped by column containing birthplace coordinates
Figure 1. World's concentration of top Rugby Union players.
The map do not show which player plays for which country. To show that a straight line is drawn from each player's birthplace to the country's capital, the player plays for (cf. Figure 2).
Figure 2. The origin of top Rugby Union players by federation.
Some lines look strange and the problem is particularly evident around New-Zealand-Samoa-Tonga-Fiji. For example it seems that many Samoan players were born at high ocean (cf. Figure 3).
Figure 3. The origin of top Rugby Union Samoan players.
BTW mapping Samoans one-by-one is OK (try it). The problem is when all rows are mapped together by Visualize→Map function.
Accidentaly around 600 miles to the East of New Zealand lies antipodal meridian ie. a meridian which is diametrically opposite the Greenwich meridian (a pime one). The longitude of points lying on antipodal meridian can be referenced (at least in GoogleMaps) both as -180° or 180° (ie. -36,-180° and -36,+180° refers to the same place). Perhaps it is the cause of observed errors...
Wprawdzie Sejm RP szybkimi krokami zmierza w stronę wybranego w 1936 r. Reichstagu określanego jako ,,najlepiej opłacany męski chór ma świecie'' (The best paid male chorus in the world -- określenie użyte podobno w amerykańskim czasopiśmie The Literary Digest) i być może już w kadencji ósmej ten stan ideału zostanie osiągnięty. Ale zanim to nastąpi zbierzmy dane dotyczące posłów wybranych do Sejmu 7 kadencji.
Zaczynam od ściągnięcia listy stron bibliograficznych posłów:
## Najpierw zestawienie posłów: wget -U XX http://www.sejm.gov.pl/sejm7.nsf/poslowie.xsp?type=A -O lista-poslow.html ## Potem dla każdego posła z zestawiena: cat lista-poslow.html | perl -e ' undef $/; $_ = <>; s/\n/ /g; while ($_ =~ m@href="/sejm7.nsf/posel.xsp\?id=([0-9]+)\&type=A@g ) { $id = $1; ## id posła $pos = "http://www.sejm.gov.pl//sejm7.nsf/posel.xsp?id=$id&type=A"; print STDERR $pos, "\n"; system ('wget', '-U', 'XXX', "-O", "7_$id.html", $pos); sleep 3; ## --let's be a little polite-- }
Powyższe pobiera 460 stron, każda zawierająca biografię jakiegoś posła.
Z kolei poniższy skrypt wydłubuje co trzeba (data scraping) ze strony zawierających biografię pojedynczego posła (np. tego).
#!/usr/bin/perl # use Storable; use Google::GeoCoder::Smart; $geo = Google::GeoCoder::Smart->new(); # Domyślny kraj my $GeoCodeCacheName = 'geocode.cache'; my $NewCoordinatesFetched=0; # global flag my $kraj = 'Polska'; my $SLEEP_TIME = 3 ; # Rok wyborów my $baseyr = 2011; my ($data_day, $data_mc, $data_yr); undef $/; $_ = <>; s/\n/ /g; # Retrieve geocode cash (if exists) # http://curiousprogrammer.wordpress.com/2011/05/11/faking-image-based-programming-in-perl/ my %hash = %{ retrieve("$GeoCodeCacheName") } if ( -f "$GeoCodeCacheName" ) ; if (m@<h2>([^<>]+)</h2>@) { $posel = recode($1); } # zgadnij płeć patrząc na imię (jeżeli kończy się na a->kobieta): my @tmp_posel = split " ", $posel; if ( $tmp_posel[0] =~ m/a$/ ) { $sex='K' } else { $sex='M' } # id posła: if (m@http://www.sejm.gov.pl/sejm7.nsf/posel.xsp\?id=([0-9]+)@) {$id = $1; } # liczba oddanych na posła głosów: if (m@<p class="left">Liczba g\łos\ów:</p>[ \n\t]*<p class="right">([^<>]+)</p>@) { $glosy = $1 ; } # Data i miejsce urodzenia:</p><p class="right">29-06-1960<span>, </span>Gdańsk</p> if (m@Data i miejsce urodzenia:[ \n\t]*</p>[ \n\t]*<p class="right">([0-9\-]+)<span>,[ \n\t]*\ [ \n\t]*</span>([^<>]+)</p>@ ) { $data = recode ($1); ($data_day, $data_mc, $data_yr) = split "-", $data; ##print STDERR "R/M/D: $data_day, $data_mc, $data_yr\n"; $mce = recode ($2); } # Zawód: if (m@<p class="left">Zaw\ód:</p>[ \nt]*<p class="right">([^<>]+)</p>@ ) { $zawod = recode($1) ; } if (m@klub.xsp\?klub=([^"<>]+)@ ) { $klub = recode($1); } # Klub poselski: if (m@Okr\ęg wyborczy:</p>[ \t\n]*<p class="right">([^<>]+)</p>@) { $okr = recode($1); ## Pierwszy wyraz to numer okręgu, reszta nazwa (może być wielowyrazowa) ($okrnr, $okrnz) = $okr =~ m/([^ \n\t]+)[ \n\t]+(.+)/; ## -- sprawdzić czy działa -- } # Poprawienie błędów if ($mce =~ /Ostrowiec Świetokrzyski/) {$mce = 'Ostrowiec Świętokrzyski' } elsif ($mce =~ /Stargard Szaczeciński/) {$mce = 'Stargard Szczeciński' ; } elsif ($mce =~ /Białograd/ ) {$mce = 'Białogard'; } elsif ($mce=~ /Szwajcaria/) {$mce = 'Szwajcaria,Suwałki' } elsif ($mce=~ /Kocierz Rydzwałdzki/) { $mce= "Kocierz Rychwałdzki" } elsif ($mce=~ /Stąporów/) { $mce= "Stąporków" } elsif ($mce=~ /Szmotuły/) { $mce= "Szamotuły" } # Wyjątki: if ($posel=~ /Arkady Fiedler/ ) {$kraj = 'Wielka Brytania' } elsif ($posel=~ /Vincent-Rostowski/) {$kraj = 'Wielka Brytania' } elsif ($mce=~ /Umuahia/) {$kraj = 'Nigeria' } elsif ($posel=~ /Munyama/) {$kraj ='Zambia'; } ## Imię kończy się na `A' ale to facet: elsif ($posel=~ /Kosma Złotowski/ ) {$sex = "M"} # Sprawdź czy data jest zawsze w formacie dd-mm-yyyy: if ($data_yr < 1900) { print STDERR "*** $data_yr: $data for $posel ($id)\n"; } # geokodowanie (uwaga na limit) my $coords = addr2coords($mce); my $wiek = $baseyr - $data_yr; my $coords_okr = addr2coords($okrnz, 'r'); ($tmp_lat, $tmp_lng) = split " ", $coords; my $kml_line = "<LineString><coordinates>$tmp_lng,$tmp_lat $coords_okr</coordinates></LineString>"; print STDERR "$id : $sex : $posel : $wiek\n"; print "$id;$sex;$posel;$data;$wiek;$mce,$kraj;$coords;$klub;$glosy;$zawod;$okrnr;$okrnz;$kml_line\n"; ## Retrieve geocode cash if ($NewCoordinatesFetched) { store(\%hash, "$GeoCodeCacheName"); } ## ## ## ## ## ## sub recode { my $s = shift; $s =~ s/\ / /g; $s =~ s/\ł/ł/g; $s =~ s/\ó/ó/g; $s =~ s/\ń/ń/g; $s =~ s/\Ł/Ł/g; $s =~ s/\ź/ź/g; $s =~ s/\ż/ż/g; $s =~ s/\Ś/Ś/g; $s =~ s/\Ż/Ż/g; $s =~ s/\ę/ę/g; $s =~ s/\ą/ą/g; $s =~ s/\ś/ś/g; $s =~ s/\ć/ć/g; $s =~ s/[ \t\n]+/ /g; return $s; } ## ## ## ## ## ## sub addr2coords { my $a = shift ; my $r = shift || 'n'; my ($lat, $lng) ; ##consult cache first if (exists $GeoCodeCache{"$a"} ) { ($lat,$lng) = split (" ", $GeoCodeCache{"$a"} ); } else { my ($resultnum, $error, @results, $returncontent) = $geo->geocode("address" => "$a"); $resultnum--; $resultNo=$resultnum ; if (resultNo > 0) { print STDERR "** Location $a occured more than once! **" } if ($error eq 'OK') { $NewCoordinatesFetched=1; for $num(0 .. $resultnum) { $lat = $results[$num]{geometry}{location}{lat}; $lng = $results[$num]{geometry}{location}{lng}; ##print "*** LAT/LNG:$lat $lng ERROR: $error RES: $resultNo ***\n"; } } else { print STDERR "** Location $a not found! due to $error **" } } $GeoCodeCache{"$a"} = "$lat $lng"; ## store in cache sleep $SLEEP_TIME; if ($r eq 'r' ) { return "$lng,$lat"; } # w formacie KML else { return "$lat $lng"; } }
Skrypt wydłubuje id posła (id), imię i nazwisko (imnz), datę urodzenia (data_ur),
miejsce urodzenia (mce_ur), skrót nazwy partii do której należy (partia),
liczbę oddanych na niego głosów (log), zawód (zawod),
numer okręgu wyborczego (nrokregu) i nazwę okręgu (nzokregu). Do tego
zgadywana jest płeć posłanki/posła oraz obliczany jest wiek w chwili wyboru jako $baseyr - $data_yr
, gdzie
$data_yr
to rok urodzenia.
Miejsce urodzenia oraz nazwa okręgu są geokodowane. Współrzędne miejsca urodzenia są zapisywane jako zmienna wsp. Jako zmienna odleglosc zapisywana jest linia definiowana (w formacie KML) jako: współrzędne miejsce urodzenia--współrzędne okręgu wyborczego.
<LineString><coordinates>lng1,lat1 lng2,lat2</coordinates></LineString>
Zamiana kupy śmiecia (tj. wszystkich 460 plików HTML) na plik CSV zawierający wyżej opisane dane sprowadza się teraz do wykonania:
for i in 7_*html ; do perl extract.pl $i >> lista_poslow_7_kadencji.csv ; done
Jako ciekawostkę można zauważyć, że strony sejmowe zawierają 6 oczywistych błędów: Ostrowiec Świetokrzyski, Stargard Szaczeciński, Białograd, Kocierz Rydzwałdzki, Stąporów oraz Szmotuły. Ponadto wieś Szwajcaria k. Suwałk wymagała doprecyzowania. Czterech posłów urodziło się za granicą a pan Kosma jest mężczyzną i prosta reguła: jeżeli pierwsze imię kończy się na ,,a'' to poseł jest kobietą zawiodła.
Ostatecznie wynik konwersji wygląda jakoś tak (cały plik jest tutaj):
id;plec;imnz;dat_ur;wiek;mce_ur;wsp;partia;log;zawod;nrokregu;nzokregu;odleglosc 001;M;Adam Abramowicz;10-03-1961;50;Biała Podlaska,Polska;52.0324265 23.1164689;PiS;\ 12708;przedsiębiorca;7;Chełm;<LineString><coordinates>23.1164689,52.0324265 23.4711986,51.1431232</coordinates></LineString> ...
Teraz importuję plik jako arkusz kalkulacyjny do GoogleDocs, a następnie, na podstawie tego arkusza tworzę dokument typu FusionTable. Po wizualizacji na mapie widać parę problemów -- ktoś się urodził niespodziewanie w USA a ktoś inny za Moskwą. Na szczęście takich omyłek jest tylko kilka...
Geocoder nie poradził sobie z miejscowościami: Model/prem. Pawlak, Jarosław/posłowie: Kulesza/Golba/Kasprzak, Orla/Eugeniusz Czykwin, Koło/Roman Kotliński, Lipnica/J. Borkowski, Tarnów/A. Grad. We wszystkich przypadkach odnajdywane były miejsca poza Polską -- przykładowo Orly/k Paryża zamiast Orli. Ponadto pani poseł Józefa Hrynkiewicz została prawidłowo odnaleziona w Daniuszewie ale się okazało, że ta miejscowość leży na Białorusi a nie w Polsce. Też ręcznie poprawiłem...
Ewidentnie moduł Google::GeoCoder::Smart
ustawia
geocoder w trybie partial match, w którym to
trybie Google
intelligently handles incomplete input aka stara się być mądrzejszym od pytającego.
Może w trybie full match byłoby lepiej, ale to by wymagało doczytania.
Na dziś, po prostu poprawiłem ręcznie te kilka ewidentnie błędnych przypadków.
Po poprawkach wszystko -- przynajmniej na pierwszy rzut -- oka wygląda OK
Link do tabeli jest zaś tutaj.
Nawiasem mówiąc kodowanie
dokumentów na stronach www.sejm.gov.pl
jest co najmniej dziwaczne.
Klasyczny zbiór danych pn. niemieckie łodzie podwodne z 2WWŚ wg. typu, liczby patroli, zatopionych statków i okrętów oraz -- dodane dziś -- miejsca zatopienia (źródło: www.uboat.net). W zbiorze są 1152 okręty (pomięto 14, które były eksploatowane przez Kriegsmarine ale nie były konstrukcjami niemieckimi), z tego 778 ma określoną pozycję, na której zostały zatopione (z czego kilkadziesiąt w ramach operacji DeadLight). Można zatem powiedzieć, że dane dotyczące miejsca zatopienia/zniszczenia są w miarę kompletne, bo np. Kemp (1997) podaje liczbę 784 U-bootów zniszczonych w czasie 2WWŚ (dane są różne w zależności od źródła.)
Jeżeli U-boot zaginął/przepadł bez wieści/został zniszczony, ale nie wiadomo dokładnie gdzie, to nie występuje na mapie. Operacja DeadLight to ta wielka kupa kropek ,,nad'' Irlandią....
Link do danych/mapy na serwerze Google jest tutaj.
Dane były tylko zgrubie weryfikowane, więc mogą zawierać błędy (ale ,,na oko'' jest ich niewiele)...
Kemp Paul, U-Boats Destroyed: German Submarine Losses in World Wars, US Naval Institute Press, 1997 (isbn: 1557508593)
Pod tym adresem umieściłem moje ślady GPS (głównie rowerowe) z lat 2010--2012. W formacie KML bo to jedyny -- z tego co mi się wydaje -- obsługiwany przez Google Fusion Tables (GFT).
Moja procedura skopiowania danych z urządzenia GPS (zwykle jest to Garmin Legend) do GFT jest następująca:
Skryptem obsługującym program
gpsbabel
pobieram dane z urządzenia GPS do pliku w formacie GPX.
Skryptem kml2kml.sh
zamieniam plik GPX otrzymany w pierwszym kroku
na plik w formacie KML. Ponieważ plik ten jest ogromy (przykładowo z 60 kilowego pliku GPX
gpsbabel wygenerował plik o wielkości ponad 500 kb) zostaje on za pomocą
trywialnego arkusza XSLT zamieniony na znacznie mniejszą wersję uproszczoną:
#!/bin/bash # KMLSTYLE=~/share/xml/kml2kml.xsl # # Domyslna liczba punktow na sladzie COUNT=99 ## Domyslna nazwa Placemarka NAME=`date +%Y%m%d_%H`; while test $# -gt 0; do case "$1" in -name) shift; NAME="$1";; -name*) NAME="`echo :$1 | sed 's/^:-name//'`";; -max) shift; COUNT="$1";; -max*) COUNT="`echo :$1 | sed 's/^:-max//'`";; *) FILE="$1";; esac shift done ## Usun rozszerzenie z nazwy pliku wejsciowego OUT_FILE="${FILE%.*}" TMP_FILE=/tmp/${OUT_FILE}.kml.tmp echo "Converting $FILE to ${TMP_FILE}..." if [ -f $FILE ] ; then ## Konwersja GPX->KML gpsbabel -i gpx -f $FILE -x simplify,count=$COUNT -o kml -F $TMP_FILE else echo "*** ERROR *** File $FILE not found.... ***" ; exit fi ## Konwersja do uproszczonego KMLa (KML->KML) echo "Converting $TMP_FILE to ${OUT_FILE}.kml with maximum $COUNT points..." echo "KML' Placemark name is: $NAME..." xsltproc --stringparam FileName "$NAME" -o ${OUT_FILE}.kml $KMLSTYLE $TMP_FILE echo "Done..."
Zawartość arkusza XSLT zamieszczono na końcu tej notki.
Do interakcji z Fusion Tables wykorzystam zmodyfikowany skrypt (nazwany ftquery.sh
)
znaleziony na stronach
code.google.com:
#!/bin/bash # # Copyright 2011 Google Inc. All Rights Reserved. # Author: arl@google.com (Anno Langen) # http://code.google.com/intl/pl/apis/fusiontables/docs/samples/curl.html # -- Modified by TP -- MY_QUERY=$* function ClientLogin() { password='?1234567890?' email='looseheadprop1@gmail.com' local service=$1 curl -s -d Email=$email -d Passwd=$password -d \ service=$service https://www.google.com/accounts/ClientLogin | tr ' ' \n | grep Auth= | sed -e 's/Auth=//' } function FusionTableQuery() { local sql=$1 curl -L -s -H "Authorization: GoogleLogin auth=$(ClientLogin fusiontables)" \ --data-urlencode sql="$sql" https://www.google.com/fusiontables/api/query } FusionTableQuery "$MY_QUERY"
Teraz tworzę nową tabelę (wyklikowując co trzeba w przeglądarce) MyTracks
o następującej strukturze:
## Korzystamy z skryptu ftquery.sh ./ftquery.sh SHOW TABLES table id,name 2590817,MyTracks ## Tabela MyTracks ma zatem id=2590817 ./ftquery.sh DESCRIBE 2590817 column id,name,type col4,Date,string col0,Start,string col1,Stop,string col2,Location,location col3,Description,string
Kolumny zawierają odpowiednio: datę (Date
), czas
pierwszego
wpisu na śladzie GPX (Start
), czas ostatniego wpisu na
śladzie GPX (Stop
), ślad (Location
) oraz opis (Description
).
Kolumny Tabeli w Google Fusin Tables mogą być albo napisami (STRING
),
liczbami (NUMBER
), zawierać dane przestrzenne (LOCATION
) albo czas (DATETIME
).
Wstępne eksperymenty
z typem DATETIME
wskazują,
że jest z nim jakiś problem.
Primo
format czasu jest dość dziwaczny (np. gpsbabelowy zapis: 2012-01-07T09:19:47Z
nie jest rozpoznawany).
Także późniejsze filtrowanie w oparciu
o kolumnę DATETIME
też jakoś nie wychodzi, nawet jak zapisałem
datę w formacie YYY.MM.DD
,
który wg. dokumentacji
powinien być rozpoznawany.
Nie badając sprawy dogłębnie, zmieniłem po prostu
typ kolumn Date
, Start
, Stop
na STRING
.
Cytując za dokumentacją GFT: In a column of type LOCATION, the value can be a string containing an address, city name, country name, or latitude/longitude pair. The string can also use KML code to specify a point, line, or polygon... (cf. SQL API: Reference ). Zatem żeby wstawić linię śladu wystarczy przesłać napis zawierający współrzędne opakowane w następujący sposób:
<lineString><coordinates>lng,lat[,alt] lng,lat[,alt]...<coordinates></lineString>
Do tego służy następujący skrypt:
#!/bin/bash # STYLE=~/share/xml/gpxtimestamp.xsl MYTRACKS_TABLE_ID="2590817" GPX_FILE=$1 KML_FILE="${GPX_FILE%.*}".kml ## Ustal czas od--do sladu STARTTIME=`xsltproc --param Position '"First"' $STYLE $GPX_FILE` STOPTIME=`xsltproc --param Position '"Last"' $STYLE $GPX_FILE` DATE=`xsltproc --param Position '"First"' --param Mode '"Date"' $STYLE $GPX_FILE` if [ ! -f $GPX_FILE ] ; then echo "*** Error *** No GPX file: $GPX_FILE" ; exit ; fi if [ ! -f $KML_FILE ] ; then echo "*** Error *** No KML file: $KML_FILE" ; exit ; fi ## ## Wycinamy nastepujacym skryptem z uproszczonego pliku KML: TRACK=`perl -e 'undef $/; $t = <>; $t =~ m/(<coordinates>.*<\/coordinates>)/s; \ $t = $1; $t =~ s/[ \t\n]+/ /gm; print $t;' $KML_FILE` TRACK="<LineString>$TRACK</LineString>" ftquery.sh "INSERT INTO $MYTRACKS_TABLE_ID (Date, Start, Stop, Location, Description) VALUES ('$DATE', '$STARTTIME', '$STOPTIME', '$TRACK', '') "
I działa, tj. przesyła co trzeba na moje konto Google.
Google się chwali, że GFT radzi sobie z ogromnymi zbiorami danych. Mój ma -- na tem chwilem -- ponad 150 wierszy, a w każdym 99 punktów na śladzie, co daje łącznie approx 15,000 punktów do wykreślenia. Firefox/Opera coś tam wyświetla ale niewiele widać. Chrome radzi sobie najlepiej -- faktycznie da się to obejrzeć, przesuwać, powiększać... Wprawdzie nic z tych danych nie wynika, ale to już nie jest pytanie do Google.
Także filtr w połączeniu z mapą działa w Firefoksie i Operze tak sobie.
Wybieram zbiór tras,
np. za pomocą warunku Date < 20100901
(na tem chwilem jest to 7 wierszy)
następnie klikam Visualise Map i guzik -- nic nie widać.
Ale znowu w Chrome jest znacznie lepiej.
Dowód w postaci zrzutu ekranu obok.
Także wklejony poniżej link do mapy (pobrany
przez kliknięcie w guzik Get embeddable link) daje pozytywny wynik:
Przynajmniej w mojej wersji FF (8.0).
Zamiana pliku KML na uproszczony plik KML:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:kml="http://www.opengis.net/kml/2.2" > <xsl:output method="xml"/> <xsl:param name='FileName' select="'??????'"/> <xsl:template match="/"> <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"> <Document> <Placemark> <name><xsl:value-of select="$FileName"/></name> <MultiGeometry><LineString> <tessellate>1</tessellate> <coordinates> <xsl:for-each select="//kml:Placemark//kml:LineString"> <xsl:value-of select="kml:coordinates"/> </xsl:for-each> </coordinates></LineString></MultiGeometry></Placemark> </Document></kml> </xsl:template> </xsl:stylesheet>
Wydrukowanie daty/czasu z pliku w formacie GPX:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:gpx="http://www.topografix.com/GPX/1/0"> <xsl:output method="text"/> <xsl:param name='Position' select="'First'"/> <xsl:param name='Mode' select="'Time'"/> <xsl:template match="/" > <xsl:for-each select='//*[local-name()="trkpt"]'> <xsl:choose> <xsl:when test="position()=1 and $Position='First' and $Mode='Time'"> <xsl:value-of select='substring(*[local-name()="time"], 12, 8)'/> <xsl:text> </xsl:text> </xsl:when> <xsl:when test="position()=1 and $Position='First' and $Mode='Date'"> <xsl:value-of select='translate(substring(*[local-name()="time"], 1, 10), "-", "")'/> <xsl:text> </xsl:text> </xsl:when> <xsl:when test="position()=last() and $Position='Last'"> <xsl:value-of select='substring(*[local-name()="time"], 12, 8)'/> <xsl:text> </xsl:text> </xsl:when> </xsl:choose> </xsl:for-each> </xsl:template> </xsl:stylesheet>