Weblog Tomasza Przechlewskiego [Zdjęcie T. Przechlewskiego]


scrum
random image [Photo gallery]
Zestawienie tagów
1-wire | 18b20 | 1wire | 2140 | 3rz | adamowicz | afera | alsamixer | amazon | amber | amman | anniversary | antypis | apache | api | applebaum | arm | armenia | astronomy | asus | atom.xml | awk | aws | bachotek | bakłażan | balcerowicz | balta | banan | bash | batumi | berlin | białowieża | białystok | bibtex | bieszczady | birzeit | biznes | blogger | blogging | blosxom | bme280 | bono | borne-sulinowo | breugel | bt747 | budapeszt | budyniowo | budyń | bursztyn | campagnolo | canon | cedewu | chaos | chello | chiller | chillerpl | chown | christophe dominici | chujowetaśmy | ciasto | cmentarz | contour | coronavirus | covi19 | covid | covid19 | cron | css | csv | cukinia | curl | cycling | d54250wykh | darkages | dbi | debian | dejavu | dhcp | dht22 | dia | docbook | dom | dp1500 | ds18b20 | duda | dulkiewicz | dulkiewiczowa | dyndns | dynia | ebay | economy | ecowitt | ekonomia | elka | elm | emacs | emacs23 | english | ep | erasmus | erasmusplus | ess | eu | eurostat | excel | exif | exiftool | f11 | fc | fc11 | fc15 | fc29 | fc5 | fc8 | fedora | fedora21 | fenix | ffmpeg | finepix | firefox | flickr | folau | fontforge | fontspec | fonty | food | fop | forms | foto | france | francja | fripp | froggit | fuczki | fuji | fuse | gammu | garden | garmin | gas | gawk | gazwyb | gdańsk | gdynia | gender | geo | geocoding | georgia | gft | ggplot | ghost | git | github | gmail | gmaps | gnokii | gnus | google | google apps script | googlecl | googleearth | googlemaps | gotowanie | gphoto | gphoto2 | gps | gpsbabel | gpsphoto | gpx | gpx-viewer | greasemonkey | gruzja | grzyby | gus | gw1000 | haldaemon | handbrake | helsinki | hhi | historia | history | hitler | holocaust | holokaust | hp1000se | hpmini | humour | iblue747 | ical | iiyama | ikea | imagemagick | imap | inkscape | inne | internet | j10i2 | javascript | jhead | jifna | jordania | k800i | kajak | kamera | karob | kibbeh | kleinertest | kml | kmobiletools | knuth | kociewie kołem | kod | kolibki | komorowski | konwersja | krutynia | krynki | kuchnia | kurski | kłamstwo | latex | latex2rtf | latex3 | lcd | legend | lenny | lesund | lewactwo | lgbt-folly | liban | liberation | linksys | linux | lisp | lisrel | litwa | lizbona | logika | ltr | lubowla | lwp | lwów | m2wś | malta | mapquest | mapsource | maradona | marchew | marimekko | marvell | math | mathjax | mazury | mbank | mediolan | mencoder | mevo | mex | mh17 | michalak | michlmayr | microsoft | monitor | mp4box | mplayer | ms | msc | mssql | msw | mswindows | mtkbabel | museum | muzyka | mymaps | mysql | mz | nafisa | nanopi | natbib | navin | neapol | nekrolog | neo | neopi | netbook | niemcy | niemieckie zbrodnie | nikon | nmea | nowazelandia | nuc | nxml | oauth | oauth2 | obituary | ocr | odessa | okular | olympus | ooffice | ooxml | opera | osm | otf | otftotfm | other | ov5647 | overclocking | ozbekiston | padwa | palestyna | panoramio | paryż | pdf | pdfpages | pdftex | pdftk | pedophilia | perl | photo | photography | pi | picasa | picasaweb | pim | pine | pis | pit | pizero | plain | plotly | pls | plugin | po | podcast | podlasie | podróże | pogoda | politics | polityka | polsat | portugalia | postęp | powerpoint | połtawa | prelink | problem | propaganda | pseudointeligencja | pstoedit | putin | python | pywws | r | r1984 | radio | random | raspberry | raspberry pi | raspberrypi | raspbian | refugees | relaxng | ridley | router | rower | rowery | roztocze | rpi | rsync | rtf | ruby | rugby | rumunia | russia | rwc | rwc2007 | rwc2011 | rwc2019 | ryga | rzym | salerno | samba | sds011 | selenium | sem | senah | sernik | sheevaplug | sienkiewicz | signature | sikorski | sks | skype | skytraq | smoleńsk | sqlite | srtm | sshfs | ssl | staszek wawrykiewicz | statistcs | statistics | stats | statystyka | stix | stretch | supraśl | suwałki | svg | svn | swanetia | swornegacie | szwajcaria | słowacja | tallin | tbilisi | terrorism | tesseract | tex | texgyre | texlive | thunderbird | tomato | totalnaopozycja | tourism | tramp | trang | transylwania | truetype | trzaskowski | ttf | turcja | turkey | turystyka | tusk | tv | tv5monde | tweepy | twitter | tykocin | typetools | ubuntu | uchodźcy | udev | ue | ukraina | umap | unix | upc | updmap | ups | utf8 | uzbekistan | varia | video | vienna | virb edit | virbedit | vostro | wammu | wdc | wdfs | weather | weathercloud | webcam | webdav | webscrapping | weewx | wenecja | wh2080 | wiedeń | wikicommons | wilno | win10 | windows | windows8 | wine | wioślarstwo | wojna | word | wordpress | wrt54gl | ws1080 | wtyczka | wunderground | ww2 | www | wybory | wybory2015 | włochy | węgry | xemex | xetex | xft | xhtml | xine | xml | xmllint | xsd | xslt | xvidtune | youtube | yum | zaatar | zakopane | zakupy | zawodzie | zdf | zdrowie | zeropi | zgarden | zgony | zprojekt | łeba | łotwa | świdnica | żywność
Archiwum
06/2023 | 02/2023 | 01/2023 | 11/2022 | 10/2022 | 09/2022 | 07/2022 | 06/2022 | 04/2022 | 03/2022 | 02/2022 | 12/2021 | 09/2021 | 03/2021 | 01/2021 | 12/2020 | 11/2020 | 10/2020 | 09/2020 | 08/2020 | 07/2020 | 04/2020 | 03/2020 | 02/2020 | 01/2020 | 12/2019 | 11/2019 | 10/2019 | 09/2019 | 08/2019 | 07/2019 | 06/2019 | 04/2019 | 02/2019 | 01/2019 | 12/2018 | 11/2018 | 10/2018 | 09/2018 | 08/2018 | 07/2018 | 05/2018 | 04/2018 | 03/2018 | 02/2018 | 01/2018 | 11/2017 | 10/2017 | 09/2017 | 08/2017 | 07/2017 | 06/2017 | 05/2017 | 04/2017 | 03/2017 | 02/2017 | 01/2017 | 12/2016 | 11/2016 | 10/2016 | 09/2016 | 08/2016 | 06/2016 | 05/2016 | 04/2016 | 02/2016 | 12/2015 | 11/2015 | 09/2015 | 07/2015 | 06/2015 | 05/2015 | 02/2015 | 01/2015 | 12/2014 | 09/2014 | 07/2014 | 06/2014 | 04/2014 | 02/2014 | 01/2014 | 12/2013 | 11/2013 | 10/2013 | 09/2013 | 08/2013 | 07/2013 | 05/2013 | 04/2013 | 03/2013 | 02/2013 | 01/2013 | 12/2012 | 11/2012 | 10/2012 | 09/2012 | 08/2012 | 07/2012 | 05/2012 | 03/2012 | 02/2012 | 01/2012 | 12/2011 | 11/2011 | 10/2011 | 09/2011 | 08/2011 | 07/2011 | 06/2011 | 05/2011 | 04/2011 | 03/2011 | 02/2011 | 01/2011 | 12/2010 | 11/2010 | 10/2010 | 09/2010 | 08/2010 | 07/2010 | 06/2010 | 05/2010 | 04/2010 | 03/2010 | 02/2010 | 01/2010 | 12/2009 | 11/2009 | 10/2009 | 09/2009 | 08/2009 | 07/2009 | 06/2009 | 05/2009 | 04/2009 | 03/2009 | 02/2009 | 01/2009 | 12/2008 | 11/2008 | 10/2008 | 09/2008 | 08/2008 | 07/2008 | 06/2008 | 05/2008 | 04/2008 | 03/2008 | 02/2008 | 01/2008 | 12/2007 | 11/2007 | 10/2007 | 09/2007 | 08/2007 | 07/2007 |
O stronie
wykorzystywany jest blosxom plus następujące wtyczki: tagging, flatarchives, rss10, lastbuilddatexhtmlmime. Niektóre musiałem dopasować nieco do swoich potrzeb. Więcej o blosxom jest tutaj
Subskrypcja
RSS 1.0
Wybory parlamentarne z 2015 w woj pomorskim

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.

url | Tue, 27/02/2018 10:01 | tagi: , , ,
Hojarskiej ekstremalnie skośny rozkład głosów

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).

Analiza przestrzenna

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.

url | Sun, 25/02/2018 17:38 | tagi: ,
Afera madrycka: taka tam analiza wyjazdów posłów 7 kadencji

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:

Edytuj→Znajdź i zamień
usuwam odstępy, tj. zamieniam spację na pusty napis
Format→Komórki
wybieram numer z dwoma miejscami po przecinku.

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ą:

  1. współrzędne celu podróży (w tym celu zamieniam adres Państwo, miasto na współrzędne geograficzne korzystając z geokodera Google);

  2. 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);

  3. 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.

url | Tue, 09/12/2014 19:09 | tagi: , , , , , , ,
Where they came from?

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.

Raw data are available here: div#1 and div#2.

url | Thu, 09/02/2012 10:41 | tagi: , , , , ,
Google Fusion Tables map rendering issue?

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

url | Mon, 06/02/2012 10:54 | tagi: , , , , , ,
Trzecie ćwiczenie z Google Fusion Tables - sejm RP siódmej kadencji

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]+)\&amp;type=A@g ) {
    $id = $1; ## id posła
    $pos = "http://www.sejm.gov.pl//sejm7.nsf/posel.xsp?id=$id&amp;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\&#322;os\&oacute;w:</p>[ \n\t]*<p class="right">([^<>]+)</p>@) {
   $glosy = $1 ; }

# Data i miejsce urodzenia:</p><p class="right">29-06-1960<span>,&nbsp;</span>Gda&#324;sk</p>
if (m@Data i miejsce urodzenia:[ \n\t]*</p>[ \n\t]*<p class="right">([0-9\-]+)<span>,[ \n\t]*\&nbsp;[ \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\&oacute;d:</p>[ \nt]*<p class="right">([^<>]+)</p>@ ) { $zawod = recode($1) ; }

if (m@klub.xsp\?klub=([^"<>]+)@ ) { $klub = recode($1); }

# Klub poselski:
if (m@Okr\&#281;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/\&nbsp;/ /g; $s =~ s/\&#322;/ł/g; $s =~ s/\&oacute;/ó/g;
  $s =~ s/\&#324;/ń/g; $s =~ s/\&#321;/Ł/g; $s =~ s/\&#378;/ź/g;
  $s =~ s/\&#380;/ż/g; $s =~ s/\&#346;/Ś/g; $s =~ s/\&#379;/Ż/g;
  $s =~ s/\&#281;/ę/g; $s =~ s/\&#261;/ą/g; $s =~ s/\&#347;/ś/g;
  $s =~ s/\&#263;/ć/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.

url | Sun, 29/01/2012 21:31 | tagi: , , , , , ,
Drugie ćwiczenie z Google Fusion Tables - mapa zatopionych Ubootów

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)...

Literatura

Kemp Paul, U-Boats Destroyed: German Submarine Losses in World Wars, US Naval Institute Press, 1997 (isbn: 1557508593)

url | Wed, 11/01/2012 21:23 | tagi: , , , , , ,
Pierwsze próby z Google Fusion Tables

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:

  1. Skryptem obsługującym program gpsbabel pobieram dane z urządzenia GPS do pliku w formacie GPX.

  2. 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.

  3. 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"
    
  4. 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.

  5. 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 Fusion Table map view in Chrome
GFT map view in Chrome

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).

Arkusze XSLT wykorzystane w skryptach

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>

Dokumentacja

  1. SQL API: Developer's Guide.

  2. SQL API: Reference .

  3. Google fusion tables cheat sheet.

url | Mon, 09/01/2012 11:51 | tagi: , , , , ,