MZ to Ministerstwo Zdrowia. Specyfiką PL jest brak danych publicznych nt. Pandemii.. Na stronie GIS nie ma nic, a stacje wojewódzkie publikują jak chcą i co chcą. Ministerstwo zdrowia z kolei na swojej stronie podaje tylko dane na bieżący dzień, zaś na amerykańskim Twitterze publikuje komunikaty na temat. To co jest na stronie znika oczywiście następnego dnia zastępowane przez inne cyferki. Do tego są tam tylko dzienne dane zbiorcze o liczbie zarażonych i zmarłych, (w podziale na województwa). Nie ma na przykład wieku, płci itp... To co jest na Twitterze z kolei ma formę tekstowego komunikatu postaci: Mamy 502 nowe i potwierdzone przypadki zakażenia #koronawirus z województw: małopolskiego (79), śląskiego (77), mazowieckiego (75)... [...] Z przykrością informujemy o śmierci 6 osób zakażonych koronawirusem (wiek-płeć, miejsce zgonu): 73-K Kędzierzyn-Koźle, 75-M Łańcut, 92-K Lipie, 72-M, 87-M, 85-K Gdańsk.
Czyli podają zarażonych ogółem i w podziale na województwa oraz dane indywidualne zmarłych w postaci płci i wieku oraz miejsca zgonu (miasta żeby było inaczej niż w przypadku podawania zakażeń.)
No więc chcę wydłubać dane postaci 92-K z tweetów publikowanych
przez Ministerstwo Zdrowia. W tym celu za pomocą
tweepy
pobieram cały streamline (+3200 tweetów zaczynających się jeszcze w 2019 roku),
ale dalej to już działam w Perlu, bo w Pythonie jakby mniej komfortowo się czuję.
Po pierwsze zamieniam format json na csv:
use JSON; use Data::Dumper; use Time::Piece; use open ":encoding(utf8)"; use open IN => ":encoding(utf8)", OUT => ":utf8"; binmode(STDOUT, ":utf8"); ## ID = tweeta ID; date = data; ## repid -- odpowiedź na tweeta o numerze ID ## text -- tekst tweeta print "id;date;repid;text\n"; while (<>) { chomp(); $tweet = $_; my $json = decode_json( $tweet ); #print Dumper($json); $tid = $json->{"id"}; $dat = $json->{"created_at"}; ## Data jest w formacie rozwlekłym zamieniamy na YYYY-MM-DDTHH:MM:SS ## Fri Oct 04 14:48:25 +0000 2019 $dat = Time::Piece->strptime($dat, "%a %b %d %H:%M:%S %z %Y")->strftime("%Y-%m-%dT%H:%M:%S"); $rep = $json->{"in_reply_to_status_id"}; $ttx = $json->{"full_text"}; $ttx =~ s/\n/ /g; ## Zamieniamy ; na , w tekście żeby użyć ; jako separatora $ttx =~ s/;/,/g; #### print "$tid;$dat;$rep;$ttx\n";
Komunikaty dłuższe niż limit Twittera są dzielone na kawałki, z których każdy jest odpowiedzią na poprzedni, np:
1298900644267544576;2020-08-27T08:30:20;1298900642522685440;53-M, 78-K i 84-K Kraków. Większość osób ... 1298900642522685440;2020-08-27T08:30:20;1298900640714948608;67-K Lublin (mieszkanka woj. podkarpackiego), 85-K Łańcut,... 1298900640714948608;2020-08-27T08:30:19;1298900639586680833;kujawsko-pomorskiego (24), świętokrzyskiego (18), opolskiego... 1298900639586680833;2020-08-27T08:30:19;;Mamy 887 nowych i potwierdzonych przypadków zakażenia #koronawirus z województw: ...
Czyli tweet 1298900639586680833 zaczyna, 1298900640714948608 jest odpowiedzią na 1298900639586680833,
a 1298900642522685440 odpowiedzią na 1298900640714948608 itd. Tak to sobie wymyślili...
W sumie chyba niepotrzebnie, ale w pierwszym kroku agreguję podzielone komunikaty
w ten sposób, że wszystkie odpowiedzi są dołączane
do pierwszego tweeta (tego z pustym polem in_reply_to_status_id
):
## nextRef jest rekurencyjna zwraca numer-tweeta, ## który jest początkiem wątku sub nextRef { my $i = shift; if ( $RR{"$i"} > 0 ) { return ( nextRef( "$RR{$i}" ) ); } else { return "$i" } } ### ### ### while (<>) { chomp(); ($id, $d, $r, $t) = split /;/, $_; $TT{$id} = $t; $RR{$id} = $r; $DD{$id} = $d; } ### ### ### for $id ( sort keys %TT ) { $lastId = nextRef("$id"); $LL{"$id"} = $lastId; $LLIds{"$lastId"} = "$lastId"; } ### ### ### for $id (sort keys %TT) { ## print "### $DD{$id};$id;$LL{$id};$TT{$id}\n"; } $TTX{$LL{"$id"}} .= " " . $TT{"$id"}; $DDX{$LL{"$id"}} .= ";" . $DD{"$id"}; } ### ### ### for $i (sort keys %TTX) { $dates = $DDX{"$i"}; $dates =~ s/^;//; ## pierwszy ; jest nadmiarowy @tmpDat = split /;/, $dates; $dat_time_ = $tmpDat[0]; ($dat_, $time_) = split /T/, $dat_time_; $ffN = $#tmpDat + 1; $collapsedTweet = $TTX{$i}; print "$i;$dat_;$time_;$ffN;$collapsedTweet\n"; }
Zapuszczenie powyższego powoduje konsolidację wątków, tj. np. powyższe 4 tweety z 2020-08-27 połączyły się w jeden:
1298900639586680833;2020-08-27;08:30:19;4; Mamy 887 nowych i potwierdzonych przypadków zakażenia #koronawirus z województw: małopolskiego (233), śląskiego (118), mazowieckiego (107), [...] Liczba zakażonych koronawirusem: 64 689 /2 010 (wszystkie pozytywne przypadki/w tym osoby zmarłe).
Teraz wydłubujemy tylko tweety z frazą nowych i potwierdzonych albo nowe i potwierdzone:
## nowych i potwierdzonych albo nowe i potwierdzone ## (MZ_09.csv to CSV ze `skonsolidowanymi' wątkami) cat MZ_09.csv | grep 'nowych i pot\|nowe i pot' > MZ_09_C19.csv wc -l MZ_09_C19.csv 189 MZ_09_C19.csv
Wydłubanie fraz wiek-płeć ze skonsolidowanych wątków jest teraz proste:
perl -e ' while (<>) { ($i, $d, $t, $c, $t) = split /;/, $_; while ($t =~ m/([0-9]+-[MK])/g ) { ($w, $p) = split /\-/, $1; print "$d;$w;$p\n"; } }' MZ_09_C19.csv > C19D.csv wc -l C19PL_down.csv 1738
Plik wykazał 1738 osób. Pierwsza komunikat jest z 16 kwietnia. Ostatni z 31. sierpnia. Pierwsze zarejestrowane zgony w PL odnotowano 12 marca (albo 13 nieważne). W okresie 12 marca --15 kwietnia zmarło 286 osób. Dodając 286 do 1738 wychodzi 2024. Wg MZ w okresie 12.03--31.08 zmarło 2039. Czyli manko wielkości 15 zgonów (około 0,5%). No trudno, nie chce mi się dociekać kto i kiedy pogubił tych 15...
Równie prostym skryptem zamieniam dane indywidualne na tygodniowe
#!/usr/bin/perl -w use Date::Calc qw(Week_Number); while (<>) { chomp(); ##if ($_ =~ /age/) { next } ## my ($d, $w, $p ) = split /;/, $_; my ($y_, $m_, $d_) = split /\-/, $d; my $week = Week_Number($y_, $m_, $d_); $DW{$week} += $w; $DN{$week}++; $DD{$d} = $week; $YY{$week} = 0; $YY_last_day{$week} = $d; ## wiek wg płci $PW{$p} += $w; $PN{$p}++; } for $d (keys %DD) { $YY{"$DD{$d}"}++; ## ile dni w tygodniu } print STDERR "Wg płci/wieku (ogółem)\n"; for $p (keys %PW) { $s = $PW{$p}/$PN{$p}; printf STDERR "%s %.2f %i\n", $p, $s, $PN{$p}; $total += $PN{$p}; } print STDERR "Razem: $total\n"; print "week;deaths;age;days;date\n"; for $d (sort keys %DW) { if ($YY{$d} > 2 ) {## co najmniej 3 dni $s = $DW{$d}/$DN{$d}; printf "%s;%i;%.2f;%i;%s\n", $d, $DN{$d}, $s, $YY{$d}, $YY_last_day{$d}; } }
Co daje ostatecznie (week
-- numer tygodnia w roku;
meanage
-- średni wiek zmarłych;
deaths
-- liczba zmarłych w tygodniu;
days
-- dni w tygodniu;
date
-- ostatni dzień tygodnia):
week;deaths;meanage;days;date 16;55;77.07;4;2020-04-19 17;172;75.09;7;2020-04-26 18;144;77.29;7;2020-05-03 19;123;76.46;7;2020-05-10 20;126;76.40;7;2020-05-17 21;71;76.37;7;2020-05-24 22;68;78.12;7;2020-05-31 23;93;75.73;7;2020-06-07 24;91;75.93;7;2020-06-14 25;109;77.24;7;2020-06-21 26;83;75.06;7;2020-06-28 27;77;74.09;7;2020-07-05 28;55;76.91;7;2020-07-12 29;54;77.33;7;2020-07-19 30;48;76.52;7;2020-07-26 31;60;74.88;7;2020-08-02 32;76;77.17;7;2020-08-09 33;71;73.11;7;2020-08-16 34;77;75.61;7;2020-08-23 35;79;74.33;7;2020-08-30
To już można na wykresie przedstawić:-)
library("dplyr") library("ggplot2") library("scales") ## spanV <- 0.25 d <- read.csv("C19D_weekly.csv", sep = ';', header=T, na.string="NA") first <- first(d$date) last <- last(d$date) period <- sprintf ("%s--%s", first, last) d$deaths.dailymean <- d$deaths/d$days cases <- sum(d$deaths); max.cases <- max(d$deaths) note <- sprintf ("N: %i (source: twitter.com/MZ_GOV_PL)", cases) pf <- ggplot(d, aes(x= as.Date(date), y=meanage)) + geom_bar(position="dodge", stat="identity", fill="steelblue") + scale_x_date( labels = date_format("%m/%d"), breaks = "2 weeks") + scale_y_continuous(breaks=c(0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80)) + xlab(label="week") + ylab(label="mean age") + ggtitle(sprintf ("Mean age of COVID19 fatalities/Poland/%s", period), subtitle=note ) note <- sprintf ("N: %i (source: twitter.com/MZ_GOV_PL)", cases) pg <- ggplot(d, aes(x= as.Date(date), y=deaths.dailymean)) + geom_bar(position="dodge", stat="identity", fill="steelblue") + scale_x_date( labels = date_format("%m/%d"), breaks = "2 weeks") + scale_y_continuous(breaks=c(0,5,10,15,20,25,30,35,40,45,50)) + xlab(label="week") + ylab(label="daily mean") + ggtitle(sprintf("Daily mean number of COVID19 fatalities/Poland/%s", period), subtitle=note ) ggsave(plot=pf, file="c19dMA_w.png") ggsave(plot=pg, file="c19dN_w.png")
Wynik tutaj:
Średnia Wg płci/wieku/ogółem. Ogółem -- 75,9 lat (1738 zgonów) w tym: mężczyźni (914 zgonów) -- 73.9 lat oraz kobiety (824) -- 78.4 lat. Stan pandemii na 31.08 przypominam. Apogeum 2 fali... Średnia wieku w PL (2019 rok) 74,1 lat (M) oraz 81,8 lat (K).
Ze strony https://www.rugbyworldcup.com/teams/TEAM
(gdzie TEAM = england, georgia itd)
można ściągnąć dane każdej drużyny. Ja ściągnąłem 14 września wszystkie 20 stron dla 20 drużyn
biorących udział w turnieju
i się okazało po obejrzeniu pliku od środka, że w każdym jest explicite
dołączony JavaSciptowy fragment zawierający dane dotyczące zawodników.
(Teraz wygląda na to, że zmieniono sposób generowania
stron i JavaScriptowej wstawki nie ma.)
Jeżeli chodzi o statystki meczów, to startową jest strona
https://www.rugbyworldcup.com/matches
, z której zwykłym grepem można wydłubać URLe do
wszystkich 48 meczy. Ten plik zawiera też opisy meczów w formacie JS (kto gra z kim i kiedy),
dzięki
czemu można ściągnąć pliki dla poszczególnych meczów w bardziej cwany sposób.
Ja zrobiłem skrypt w Perlu, który tworzy plik .sh ściągający wszystkie mecze:
## ściąga tylko mecz rozegrany TODAY if [ "$TODAY" = "20190921" ]; then echo 'Download: France-Argentina => France_Argentina_0921_25292_.html' selenium_get_www_page.py 'https://www.rugbyworldcup.com/match/25292' > France_Argentina_0921_25292_.html ## wyciąga statystyki i zapisuje do pliku .csv perl html2csv.pl -f France_Argentina_0921_25292_.html > France_Argentina_0921_25292_.csv fi
Idea była taka żeby dodać plik do Crontaba na nafisie (czyli raspberry).
Codziennie po południu by się odpalał i ściągał mecze rozegrana tego dnia.
Plik HTML jest deklarowany jako xhtml i nawet jest poprawny (well-formed).
Dzięki temu dość sprawnie udało
mi się zrobić skrypt html2csv.pl
, który
wydłubuje wszystkie dane meczowe i dopisuje je do pliku w formacie CSV.
Przedostatnim krokiem jest uruchomienie skryptu R, który rysuje 6 wykresów słupkowych dla najważniejszych statystyk.
Ostatnim zaś krokiem wysyłanie tego
co zrobił R na twittera (za pomocą tweepy
, codziennie wieczorem).
Baza danych z RWC 2019 jest tutaj.
Załóżmy, że plik CSV zawiera liczbę opublikowanych twitów (dane tygodniowe). Problem: przedstawić szereg w postaci przebiegu czasowego (time plot). Taki skrypt R wymyśliłem do zrealizowania tego zadania:
require(ggplot2) args <- commandArgs(TRUE) ttname <- args[1]; file <- paste(ttname, ".csv", sep="") filePDF <- paste(ttname, ".pdf", sep="") d <- read.csv(file, sep = ';', header=T, na.string="NA", ); ## Plik CSV jest postaci: ##str(d) ## wiersze 1,2 + ostatni są nietypowe (usuwamy) rows2remove <- c(1, 2, nrow(d)); d <- d[ -rows2remove, ]; ## szacujemy prosty model trendu lm <- lm(data=d, posts ~ no ); summary(lm) posts.stats <- fivenum(d$posts); posts.mean <- mean(d$posts); sumCs <- summary(d$posts); otherc <- coef(lm); # W tytule średnia/mediana i równanie trendu title <- sprintf ("Weekly for %s # me/av = %.1f/%.1f (y = %.2f x + %.1f)", ttname, sumCs["Median"], sumCs["Mean"], otherc[2], otherc[1] ); ##str(d$no) ## Oś x-ów jest czasowa ## Skróć yyyy-mm-dd do yy/mmdd d$date <- sub("-", "/", d$date) ## zmienia tylko pierwszy rr-mm-dd d$date <- sub("-", "", d$date) ## usuwa mm-dd d$date <- gsub("^20", "", d$date) ## usuwa 20 z numeru roku 2018 -> 18 weeks <- length(d$no); ## https://stackoverflow.com/questions/5237557/extract-every-nth-element-of-a-vector ## Na skali pokaż do 20 element /dodaj ostatni `na pałę' (najwyżej zajdą na siebie) ## możnaby to zrobić bardziej inteligentnie ale nie mam czasu scaleBreaks <- d$no[c(seq(1, weeks, 20), weeks)]; scaleLabs <- d$date[c(seq(1, weeks, 20), weeks)]; ggplot(d, aes(x = no, y = posts)) + geom_line() + ggtitle(title) + ylab(label="#") + xlab(label=sprintf("time (yy/mmdd) n=%d", weeks )) + scale_x_continuous(breaks=scaleBreaks, labels=scaleLabs) + geom_smooth(method = "lm") ggsave(file=filePDF)
Poniższy skrypt Perlowy
służy do pobierania najnowszych twitów (Tweets) użytkowników
identyfikowanych poprzez ich screen_name
. Twity
są dopisywane do bazy, która jednocześnie pełni rolę pliku
konfiguracyjnego.
Przykładowo, aby twity użytkownika maly_wacek
były
dodane do bazy należy wpisać do niej wpis (w dowolnym miejscu, dla
porządku najlepiej na początku):
INIT;maly_wacek;;INIT
Ściśle rzecz biorąc po pierwszym dodaniu do bazy, powyższy wpis jest
już niepotrzebny, ale też nie przeszkadza. Baza jest zapisywana
w taki sposób, że najnowszy tweet każdego użytkownika jest na końcu,
zatem po przeczytaniu pliku, w wyniku przypisania
$Users{$tmp[1]} = $tmp[0]
(por. poniżej),
hash %Users
zawiera wszystkich użytkowników oraz id_str
ich ostatnio
pobranego twita. Zapewne niespecjalnie optymalny sposób
archiwizacji, ale prosty i działa:
#!/usr/bin/perl use Net::Twitter; # Z UTF8 w Perlu jest zawsze problem: use open ":encoding(utf8)"; use open IN => ":encoding(utf8)", OUT => ":utf8"; my $timelineBase = "timelines.log"; if ( -f "$timelineBase" ) { open (BASE, $timelineBase) || die "Cannot open: $timelineBase"; while (<BASE>) { chomp(); @tmp = split /;/, $_; $Users{$tmp[1]} = $tmp[0]; # last id_str } } close (BASE) ; ## ### #### open (BASE, ">>$timelineBase") ; my $nt = Net::Twitter->new(legacy => 0); my $nt = Net::Twitter->new( traits => [qw/API::RESTv1_1/], consumer_key => "######", consumer_secret => "######", access_token => "######", access_token_secret => "######", ); foreach $user ( keys %Users ) { my @message ; my $screen_name = $user ; my $result ; if ( $Users{$user} eq 'INIT' ) { ## max ile się da, wg dokumentacji 3200 $result = $nt->user_timeline({ screen_name => $screen_name, count=> '3200' }) } else { $result = $nt->user_timeline({ screen_name => $screen_name, since_id => $Users{$user}, }); } foreach my $tweet ( @{$result} ) { $text_ = $tweet->{text} ; $text_ =~ s/;/\,/g; $text_ =~ s/\n/ /g; $date_ = $tweet->{created_at} ; push ( @message, $tweet->{id_str} . ";" \ . "$screen_name;$date_;$text_" ); } ## Drukuj posortowane: my $tweetsC; foreach my $tweet ( sort (@message) ) { $tweetsC++ ; print BASE $tweet . "\n"; } if ( $tweetsC > 0 ) { print STDERR "fetched $tweetsC for $screen_name\n"; } } close (BASE)
Uwaga: poprzez API można pobrać twity użytkowników, którzy zablokowali nam możliwość oglądania ich konta (inna sprawa po co oglądać takiego palanta).
apps.twitter.com
Należy się zalogować na
stronie apps.twitter.com/
.
Kliknąć Create New App.
Wybrać Name (np. tprzechlewski.app
),
Description, Website i Callback URL.
Wybrać Keys and Access Tokens i pobrać wartości:
Consumer Key
oraz Consumer Secret
.
Przewinąć zawartość strony i wybrać Create my access token.
Zostaną wygenerowane Access Token
oraz Access Token Secret
,
które także należy pobrać.
Na potrzeby wyżej opisanego skryptu to wystarczy. Pobrane wartości
wstawiamy w miejsca oznaczone jako ######
Net::Twitter
Na jednym z moich komputerów ciągle działa dość archaiczna wersja Debiana Lenny:
$ cat /proc/version Linux version 2.6.32-5-kirkwood (Debian 2.6.32-30) $ cat /etc/issue Debian GNU/Linux 5.0 \n \l $ perl --version This is perl, v5.10.0 built for arm-linux-gnueabi-thread-multi Copyright 1987-2007, Larry Wall
Z poważnym obawami, że się uda spróbowałem:
cpan> install Net::Twitter Strange distribution name
Pomaga (por. tutaj):
cpan> install IO::AIO
Potem:
cpan> install YAML cpan> install Net::Twitter
Ściąga się milion pakietów. Przy testowaniu Net-HTTP-6.09
system zawisł na etapie t/http-nb.t
(pomogło Ctr-C), ale
finał był pomyślny, tj. Net::Twitter
został zaistalowany.
Mój inny system jest już nowszy a instalacja Net::Twitter
bezproblemowa:
$ cat /etc/issue Fedora release 21 (Twenty One) $ perl --version This is perl 5, version 18, subversion 4 (v5.18.4) built for x86_64-linux-thread-multi (with 25 registered patches, see perl -V for more detail) Copyright 1987-2013, Larry Wall $ yum install perl-Net-Twitter
Teraz wystarczy umieścić w crontab
na przykład taki wpis:
# 48 min po północy codziennie 48 0 * * * /home/tomek/bin/twitter.sh
Co zawiera twitter.sh
jest oczywiste