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:

seohost
  • 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

  1. Pobieranie danych z pliku z formatowaniem można zrobić przy użyciu funkcji fopen, fscanf i fclose.
  2. Do zamiany tekstu na tablicę służy funkcja explode.
  3. Aby utworzyć dowolnej wielkości tablicę i zapełnić ją dowolnymi wartościami, możemy użyć array_fill.
  4. Do sortowania tablic można użyć funkcji sort , arsort lub asort. Funkcje arsort i asort nie zmieniają indeksów tablicy po sortowaniu. Różnią się kolejnością sortowania danych.
  5. Poruszanie się po tablicy można zrealizować z wykorzystaniem funkcji next, która wskazuje na kolejny element. Funkcja reset ustawia zaś wskaźnik na pierwszą pozycję w tablicy.
  6. Pobranie aktualnej wartości z tablicy można zrobić przy użyciu funkcji current, a aktualnego klucza – przy użyciu funkcji key.
  7. Przy użyciu array_slice można wyodrębnić kawałek tablicy.
  8. Aby zsumować wszystkie wartości w tablicy możemy użyć array_sum.
  9. Do wyświetlenia zawartości zmiennej można użyć print_r.