Analiza wylosowanych liczb w PHP
Ciąg dalszy artykułu związanego z losowaniem liczb Lotto.
W tym artykule zajmiemy się analizą liczb na przykładzie prawdziwych zbiorów danych. Ponownie powtórzę, że loterie są przeznaczone dla osób pełnoletnich, a poniższa treść jest jedynie w celach edukacyjnych / rozrywkowych. Zastrzegam również, że nie można przewidzieć wyników losowania.
Co trzeba znać przed rozpoczęciem artykułu?
Podstawy PHP:
- zmienne
- tablice asocjacyjne
- pętle (
for
,while
,foreach
).
Czego się nauczysz?
- Pobieranie zawartości pliku z formatowaniem
- Wypełnianie tablicy tymi samymi wartościami
- Sortowanie tablicy
- Sprawdzanie czy element tablicy istnieje
- Poruszanie się po tablicy z wykorzystaniem wskaźników
- Wycinanie dowolnego kawałka indeksowanej tablicy
- Sumowanie elementów tablicy
- Wyświetlanie zawartości zmiennych i obiektów.
Zbiory danych
Szukałem i niestety nie znalazłem oficjalnego API Totalizatora Sportowego. Wygląda to tak jakby kiedyś było udostępnione, ale z jakiś powodów nie ma. Można oczywiście wyniki pobrać ze strony internetowej (programowo to tzw. web scrapping), ale to niech będzie temat na inny artykuł. Ściąganie stron obciąża serwery firmy od której pobieramy i z tego powodu balansujemy na granicy prawa (większe obciążenie to wyższe koszty, a przy dużej ilości zapytań do serwera możemy zostać oskarżeni nawet o atak DDOS). Dlatego takie operacje należy wykonywać „z głową”, a najlepiej po prostu korzystać z udostępnionego API.
Choć oficjalnych danych nie ma to znalazłem stronkę na której ktoś właśnie takie operacje wykonuje i udostępnia w formie plików tekstowych lub Excela – strona mbnet.com.pl. Przyjrzyjmy się plikowi tekstowemu (tutaj Duży Lotek). Końcówka pliku wygląda tak:
6880. 09.05.2023 8,16,27,28,38,49
6881. 11.05.2023 2,3,9,27,38,46
6882. 13.05.2023 2,7,8,32,41,45
6883. 16.05.2023 4,16,18,33,34,46
6884. 18.05.2023 5,7,20,29,45,48
6885. 20.05.2023 7,8,12,16,40,48
Widzimy, że poszczególne losowania są w kolejnych wierszach, a w każdym wierszu mamy numer losowania (po niej jest kropka), datę i liczby. Każda kolumna oddzielona jest spacją.
Operacje na pliku
Pobierzmy plik na dysk – tam gdzie znajduje się nasz skrypt. Do operacji na plikach służy kilka funkcji w języku PHP. Możemy odczytywać plik znak po znaku, linia po linii lub od razu pobrać całość. Funkcji realizujących to zadanie jest aż nadto i warto poświęcić temu osobny artykuł.
Do realizacji naszego celu użyjemy rzadziej używanej funkcji fscanf
. Funkcja pobiera zawartość pliku linia po linii, a poszczególne dane wrzuca np. do odpowiednich zmiennych korzystając z formatowania znanego z funkcji sprintf
.
$wylosowane = [];
$filename = 'nazwa_pliku.txt';
$plik = fopen ($filename, 'r'); // otwieramy plik w trybie do odczytu
// czytamy zawartość pliku linia po linii i zapisujemy poszczególne kolumny do zmiennych
while ( fscanf ($plik, "%d. %s %s", $nr, $data, $liczby) ) {
$wylosowane[$nr] = explode(',',$liczby); // zapisujemy liczby do tablicy
}
fclose( $plik ); // zamykamy plik
Po otwarciu pliku funkcją fopen
(w trybie tylko do odczytu) wykonujemy pętlę tak długo, aż funkcja fscanf
nie zwróci wartości false
. Czytanie pliku odbywa się przy użyciu wskaźnika (seek). Otwierając go w trybie tylko do odczytu wskaźnik ustawiony jest na jego początku. Funkcja fscanf
czyta każdy wiersz pliku i ustawia wskaźnik w nowym.. tak długo, aż wartość wskaźnika będzie większa od wielkości samego pliku (dojdziemy do końca). Wtedy pętla zakończy działanie.
fscanf
pobiera linię pliku i wyciąga z linii (po kolei):
- liczbę (
%d
) – np. 6880 – po niej jest kropka i spacja, które nas nie interesują – zapisuje to do zmiennej $nr - tekst
(%s
) – np. 09.05.2023 – tekst jest czytany do natrafienia spacji – zapisuje to do zmiennej $data - tekst (
%s
) – np. 8,16,27,28,38,49 – tekst jest czytany do natrafienia spacji – zapisuje to do zmiennej $liczby
W zmiennej $liczby
znajdują się liczby losowania. Zamieniamy je na tablicę korzystając z faktu, że poszczególne cyfry są oddzielone przecinkami. Służy do tego funkcja explode
. Zapisujemy je jako kolejny element tablicy $wylosowane
uzyskując taką strukturę:
[...,
6880 => [8,16,27,28,38,49],
6881 => [2,3,9,27,38,46],
...
6885 => [7,8,12,16,40,48]
]
Kluczami tablicy są numery losowań, a elementami jest tablica z liczbami. Chcąc przykładowo sprawdzić, trzecią liczbę w losowaniu numer 6885 możemy oczywiście napisać teraz.
echo 'Trzecia liczba w losowaniu 6885 to '.$wylosowane[6885][2];
Indeks trzeciej liczby to 2, bo tablicę są indeksowane od 0.. czyli tutaj 0,1,2,3,4,5
Przepiszmy to jeszcze do funkcji:
function getFromFile ($nazwapliku) {
$liczby = [];
$plik = fopen ($nazwapliku, 'r'); // otwieramy plik w trybie do odczytu
// czytamy zawartość pliku linia po linii i zapisujemy poszczególne kolumny do zmiennych
while ( fscanf ($plik, "%d. %s %s", $nr, $data, $liczby) ) {
$liczby [$nr] = explode(',',$liczby); // zapisujemy liczby do tablicy
}
fclose( $plik ); // zamykamy plik
return $liczby ;
}
$wylosowane = getFromFile ('nasz_plik.txt');
echo 'Trzecia liczba w losowaniu 6885 to '.$wylosowane[6885][2];yy
Najczęściej losowana liczba
Sprawdźmy, która z liczb była losowana najczęściej. W tym celu utworzymy tablicę zawierającą tyle elementów ile jest liczb i pod każdym indeksem wypiszemy zero.
$wystapienia = array_fill (1,49,0); // tworzy tablicę 1..49 i wypełnia ją zerami
foreach ($wylosowane as $losowanie) {
foreach ($losowanie as $liczba) {
$wystapienia[$liczba]++; // dodaje jeden do elementu w tablicy pod daną liczbą
}
}
array_fill
tworzy pustą tablicę składajacą się z 49 elementów w której pierwszy indeks to 1 i wypełnia ją zerami. Następnie mamy pętle, która w każdym losowaniu czyta każdą liczbę i dodaje jeden do elementu w tablicy.
Aby wyłuskać najczęściej występującą liczbę posortujmy tablicę po wartościach z zachowaniem kluczy. Służą do tego funkcje asort
(sortuje rosnąco – od najmniejszych do największych) i arsort
(sortuje malejąco – od największych do najmniejszych)
arsort ($wystapienia);
echo "Najczęściej występujące liczby to:\n";
$limit = 10;
do {
echo key ($wystapienia) . " wystąpiła " . current($wystapienia)." razy\n"; // obecny klucz i wartość
next ($wystapienia); // ustaw wskaźnik na kolejny element tablicy
$limit--;
} while ($limit > 0);
reset ($wystapienia); // ustaw wskaźnik na pierwszy element tablicy
Po tablicy można się przemieszczać na wiele różnych sposobów. W powyższym przykładzie korzystamy z faktu, że każdy element tablicy ma swój wewnętrzny wskaźnik. Funkcja key
wyświetla nazwę obecnego klucza, czyli liczby, a funkcja current
jego wartość, czyli tutaj – ilość wystąpień. Przy użyciu next
natomiast ustawiamy kolejny wskaźnik, czyli przeskakujemy na kolejny element tablicy. Na samym końcu profilaktycznie dodałem reset
, który ustawia wskaźnik na początku
Skrypt dla powyższego zestawu danych wyświetlił 10 liczb:
Najczęściej występujące liczby to:
34 wystąpiła 900 razy
17 wystąpiła 897 razy
27 wystąpiła 895 razy
21 wystąpiła 893 razy
24 wystąpiła 889 razy
38 wystąpiła 886 razy
4 wystąpiła 883 razy
25 wystąpiła 877 razy
6 wystąpiła 877 razy
1 wystąpiła 865 razy
Najczęściej losowana para
Pary liczb, czyli liczby które występują razem. W losowaniu numer 6880 (8,16,27,28,38,49) mamy następujące pary:
8,16
8,27
8,28
8,38
8,49
16,27
16,28
16,38
16,49
27,28
27,38
27,49
28,38
28,49
38,49
Wypisanie par z danego losowania można zrealizować z użyciem np. takiego zestawu pętli:
$pary = [];
for ( $one = 0; $one < 5 ; $one++ ) {
for ($two = $one+1; $two < 6; $j++) {
$pary[$wylosowane[6880][$one] . ','. $wylosowane[6880][$two]] = 1;
}
}
Pierwsza pętla wykonuje się 5 razy dla każdej liczby za wyjątkiem ostatniej. Druga pętla wewnątrz pierwszej wykonuje się na początku 5 razy, potem 4, … itd., ale dla każdej kolejnej liczby. Aby lepiej to zrozumieć spróbuj przyjrzeć się jak będą wyglądały poszczególne kroki:
$wylosowane[6880] = [8,16,27,28,38,49]
// KROK 1
$one = 0;
$two = 1;
$pary ["8,16"] = 1;
// KROK 2
$one = 0;
$two = 2;
$pary ["8,27"] = 1;
// ... KROK 5
$one = 0;
$two = 5;
$pary ["8,49"] = 1;
// KROK 6
$one = 1;
$two = 2;
$pary ["16,27"] = 1;
// KROK 7
$one = 1;
$two = 3;
$pary ["16,28"] = 1;
//...
Zapisaliśmy te liczby w tablicy, gdzie kluczem są liczby w formie tekstu oddzielone przecinkami. W ten sposób możemy łatwo dodać licznik i sumować wystąpienia. Zwróć też uwagę, że aby ten algorytm działał poprawnie to zestaw liczb musi być posortowany. Tutaj tak jest (gdyby było inaczej moglibyśmy użyć funkcji sort
). Gdyby nie było sortowania uzyskalibyśmy czasami podwójne pary np. ["16,28"]
i ["28,16"]
. Liczby te same, ale w praktyce są to dwa różne zestawy.
Rozbudujmy naszą funkcję o prawidłowy licznik i sprawdźmy wszystkie zestawy.
$pary = [];
foreach ($wylosowane as $losowanie) {
for ( $one = 0; $one < 5 ; $one++ ) {
for ($two = $one+1; $two < 6; $two++) {
if (!isset ($pary[ $losowanie[$one] . ','. $losowanie[$two] ]) ) {
$pary[$losowanie[$one] . ','. $losowanie[$two]] = 1;
}
else {
$pary[$losowanie[$one] . ','. $losowanie[$two]] += 1;
}
}
}
}
Wykonaliśmy pętle dla wszystkich losowań i dodaliśmy funkcję isset
, która sprawdza czy została już wcześniej utworzona tablica z analizowaną parą. Jeżeli nie ma to zapisujemy 1. Jeżeli jest to dodajemy 1 do wpisanej wartości.
Wyciąganie najczęściej występujących par
Najczęściej występujące pary wyciągniemy identycznie jak miało to miejsce z liczbami. Wystarczy posortować tablicę z wykorzystanie arsort
.
arsort ($pary);
echo "Najczęściej występujące pary to:\n";
$limit = 10;
do {
echo key ($pary) . " wystąpiły " . current($pary)." razy\n"; // obecny klucz i wartość
next ($pary); // ustaw wskaźnik na kolejny element tablicy
$limit--;
} while ($limit > 0);
reset ($pary); // ustaw wskaźnik na pierwszy element tablicy
Wynik działania programu na przykładzie prezentowanych danych:
9,18 wystąpiły 115 razy
21,39 wystąpiły 115 razy
1,17 wystąpiły 114 razy
6,16 wystąpiły 114 razy
31,42 wystąpiły 113 razy
4,20 wystąpiły 113 razy
4,27 wystąpiły 113 razy
27,34 wystąpiły 112 razy
17,21 wystąpiły 112 razy
6,42 wystąpiły 112 razy
Najrzadziej występujące pary
Tutaj działamy podobnie jak poprzednio, ale zanim zaczniemy analizę musimy utworzyć zestaw wszystkich kombinacji i wypełnić go zerami (aby uwzględnić również takie pary, które nie wystąpiły nigdy.
$pary = [];
for ( $one = 1; $one < 49 ; $one++ ) {
for ($two = $one+1; $two < 50; $two++) {
$pary[$one . ','. $two] = 0;
}
}
Możemy nieco odchudzić teraz pętlę, która zajmuje się zliczaniem wystąpień. Nie ma potrzeby sprawdzania czy tablica istnieje (chyba, że dla celów kontrolnych).
foreach ($wylosowane as $losowanie) {
for ( $one = 0; $one < 5 ; $one++ ) {
for ($two = $one+1; $two < 6; $two++) {
$pary[$losowanie[$one] . ','. $losowanie[$two]] += 1;
}
}
}
Poprawiamy także element związany z wyświetlaniem – korzystamy z funkcji asort
, aby posortować od najmniejszej do największej (rosnąco).
asort ($pary);
echo "Najrzadziej występujące pary to:\n";
$limit = 10;
do {
echo key ($pary) . " wystąpiły " . current($pary)." razy\n"; // obecny klucz i wartość
next ($pary); // ustaw wskaźnik na kolejny element tablicy
$limit--;
} while ($limit > 0);
reset ($pary); // ustaw wskaźnik na pierwszy element tablicy
I wynik na przykładzie posiadanych danych:
Najrzadziej występujące pary to:
40,48 wystąpiły 59 razy
42,48 wystąpiły 61 razy
32,43 wystąpiły 62 razy
23,43 wystąpiły 62 razy
7,30 wystąpiły 63 razy
9,12 wystąpiły 65 razy
11,47 wystąpiły 66 razy
5,25 wystąpiły 66 razy
13,48 wystąpiły 66 razy
23,45 wystąpiły 66 razy
Testowanie skuteczności zgadywania
Mając bazę ponad 6800 losowań możemy sprawdzić w przeszłości skuteczność działania algorytmu poprzez próbę przewidzenia wystąpienia liczb w kolejnym losowaniu. Dla uproszczenia weźmiemy 6 najrzadziej występujących liczb i sprawdzimy w każdym losowaniu w przeszłości ile udałoby nam się wygrać. Sprawdzimy algorytm dla ostatnich 6000 losowań.
Najprościej zrealizować to zadanie korzystając z już przygotowanych algorytmów i wykonywać je dla wszystkich losowań w przeszłości, przekazując każdorazowo coraz mniejszą tablicę . Jest to jednak działanie karkołomne i skrypt taki będzie się wykonywać długo.
Dużo efektywniej będzie wykonać to zadanie w trakcie zliczania. Korzystamy tu z faktu, że same losowania są posortowane od najstarszych do najnowszych.
Zmodyfikujmy skrypt:
$trafienia = array_fill(1,6,0);
$przewidywane = [];
$wystapienia = array_fill (1,49,0); // tworzy tablicę 1..49 i wypełnia ją zerami
foreach ($wylosowane as $nr => $losowanie) {
$trafionych_liczb = 0;
foreach ($losowanie as $liczba) {
$wystapienia[$liczba]++; // dodaje jeden do elementu w tablicy pod daną liczbą
if (isset ($przewidywane [$liczba]) ) $trafionych_liczb++;
}
$trafienia[$trafionych_liczb]++;
asort ($wystapienia);
$przewidywane = array_slice ($wystapienia, 0, 6, true);
}
echo "Liczba trafień dla " . array_sum($trafienia) . " zakładów\n\n";
print_r($trafienia);
Przy każdym zliczanym losowaniu wyciągamy 6 najczęściej do tej pory występujących liczb i sprawdzamy ile z nich wystąpiło w kolejnym losowaniu – zapisujemy ilość trafień w tablicy $trafienia
. Aby to zrobić sortujemy tablicę z wystąpieniami malejąco i następnie za pomocą array_slice
wycinamy pierwszych 6 liczb zaczynając od indeksu 0 (tworzymy nową tablicę z zachowaniem kluczy w tablicy – ostatni parametr ustawiony na true
). Na końcu zliczamy ile było losowań sumując wszystkie elementy tablicy $trafienia
przy użyciu array_sum
i wyświetlamy zmienną z wykorzystaniem print_r
, która wypisze nam zawartość zmiennej / tablicy / obiektu itd.
Uzyskaliśmy:
Liczba trafień dla 6885 zakładów
Array
(
[1] => 2726
[2] => 895
[3] => 116
[4] => 5
[5] => 0
[6] => 0
[0] => 3143
)
Upraszczając na podstawie obecnych kosztów Dużego Lotka (3 zł kupon), aby zarobić ok. 3634 zł. musielibyśmy wydać ok. 20 655 zł (stosując tą metodę). Oczywiście kilka pierwszych losowań jest bez sensu analizowanych, ale to i tak nie zmienia za bardzo skali straty.
Podsumowanie
- Pobieranie danych z pliku z formatowaniem można zrobić przy użyciu funkcji
fopen
,fscanf
ifclose
. - Do zamiany tekstu na tablicę służy funkcja
explode
. - Aby utworzyć dowolnej wielkości tablicę i zapełnić ją dowolnymi wartościami, możemy użyć
array_fill
. - Do sortowania tablic można użyć funkcji
sort
,arsort
lubasort
. Funkcjearsort
iasort
nie zmieniają indeksów tablicy po sortowaniu. Różnią się kolejnością sortowania danych. - Poruszanie się po tablicy można zrealizować z wykorzystaniem funkcji
next
, która wskazuje na kolejny element. Funkcjareset
ustawia zaś wskaźnik na pierwszą pozycję w tablicy. - Pobranie aktualnej wartości z tablicy można zrobić przy użyciu funkcji
current
, a aktualnego klucza – przy użyciu funkcjikey.
- Przy użyciu
array_slice
można wyodrębnić kawałek tablicy. - Aby zsumować wszystkie wartości w tablicy możemy użyć
array_sum
. - Do wyświetlenia zawartości zmiennej można użyć
print_r
.