„Click” i już – eleganckie aplikacje konsolowe w pythonie

TLDR: pakiet Click pozwala na przygotowanie w łatwy sposób aplikacji konsolowej, zarządzając m.in. parametrami

Autor zdjęcia: https://www.pexels.com/@negativespace

Czasami gdy przygotowaliśmy jakiś przydatny (bądź nie) kawałek kodu, chcielibyśmy go udostępnić światu. Dobrze wówczas zadbać o wygodny interfejs do jego wykorzystywania. Jeżeli ma być to aplikacja konsolowa można w tym celu wykorzystać pakiet click

pip install click

Załóżmy, że korzystając z pakietu basketball_reference_web_scraper chcemy „wydrukować” personalia zawodnika zdobywającego najwięcej punktów w NBA – przygotowaliśmy sobie skrypt dla bieżącego sezonu.

from basketball_reference_web_scraper import client

players = pd.DataFrame(client.players_season_totals(season_end_year=2020))
players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

max_player = players.iloc[players['points'].idxmax()]
print("name:", max_player['name'], "-", "points", "in", "2020", "season:",  max_player['points'])

Automatyzacja tego zadania dla wielu sezonów do postaci wykonywalnego skryptu jest bajecznie prosta:

import click
from basketball_reference_web_scraper import client
import pandas as pd

@click.command()
@click.argument('season_end_year', type=int)
def max_player(season_end_year):
    """Print name and number of points in of player with most points in NBA season which ends in SEASON_END_YEAR 

    SEASON_END_YEAR is a int number
    """
    players = pd.DataFrame(client.players_season_totals(season_end_year=season_end_year))
    players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
    players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

    max_player = players.iloc[players['points'].idxmax()]
    print("name:", max_player['name'])
    print("points", "in", season_end_year-1, "/", season_end_year, "season:",  max_player['points'])

if __name__ == '__main__':
    max_player()

@click.command() to dekorator który zamienia funkcję poniżej w obiekt command – nie zagłębiając się w szczegóły, to konieczne aby następne polecenia zadziałały.
@click.argument() definiuje argument skryptu – wartość wpisywaną po nazwie skryptu.
Wywołanie skryptu jest już bardzo proste:

python max_player.py 2010

Co zwraca nam informację o ówczesnym królu strzelców:

name: Kevin Durant
points in 2009 / 2010 season: 2472

Proste jest także dodawanie „parametrów” skryptu – załóżmy, że chcemy dodać możliwość sprawdzania innych statystyk niż punkty. Służy do tego dekorator @click.option()

import click
from basketball_reference_web_scraper import client
import pandas as pd

@click.command()
@click.option('--stat', '-s', type=click.STRING, help="Stat used to determine max player", default="points")
@click.argument('season_end_year', type=int)
def max_player(season_end_year, stat):
    """Print name and number of points (assists, rebounds) of player with most points (assists, rebounds) in NBA season which ends in SEASON_END_YEAR 

    SEASON_END_YEAR is an int number
    """

    if stat not in ("points", "assists", "rebounds", "steals", "blocks"):
        print("stat not recognized")

    players = pd.DataFrame(client.players_season_totals(season_end_year=season_end_year))
       
    players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
    players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

    max_player = players.iloc[players[stat].idxmax()]
    print("name:", max_player['name'])
    print(stat, "in", season_end_year-1, "/", season_end_year, "season:",  max_player['points'])

if __name__ == '__main__':
    max_player()

Dzięki zastosowaniu tego możemy sprawdzić np. kto był królem asyst w 2015…

python max_player.py --stat=assists 2015

i przypomnieć sobie „złotą erę” Chrisa Paula:

name: Chris Paul
assists in 2014 / 2015 season: 1564

Swoistą wisienką na torcie jest automatyczne generowanie helpa – wywołanie:

python max_player.py --help

zwróci nam ładnie sformatowany manual

Usage: max_player.py [OPTIONS] SEASON_END_YEAR

  Print name and number of points (assists,
  rebounds) in of player with most points in NBA
  season which ends in SEASON_END_YEAR

  SEASON_END_YEAR is a int number

Options:
  -s, --stat TEXT  Stat used to determine max
                   player

  --help           Show this message and exit.

Zastosowań clicka jest oczywiście dużo, dużo więcej – choćby wprowadzanie haseł ale o tym już najlepiej poczytać na stronie dokumentacji

Robotyka amatorska

TLDR: Opis kilku konkurencji rozgrywanych na zawodach robotów mobilnych.

Ponieważ nie samym Data Science żyje człowiek, chciałbym przybliżyć inną z moich pasji – robotykę amatorską.

Robotyka amatorska jest dość szczególnym działem robotyki. Jest to dziedzina zajmująca się robotami mobilnymi, które mają za zadanie konkurować ze sobą podczas zawodów w jednej z kilku dyscyplin. Do najbardziej znanych i popularnych konkurencji można zaliczyć m.in.:

Zawody Line Follower – w których roboty mają za zadanie poruszać się jak najszybciej po torze. Tor wyznaczany jest przez ciemną linię umieszczoną na jasnej planszy. Roboty są startowane pojedynczo, zwycięża robot, który bezbłędnie przejedzie trasę w najkrótszym czasie. W mniej „kanonicznej” odmianie tego typu zawodów (tzw. Line Follower Enhanced) na trasie umieszczane są przeszkody (np. podjazdy, kurtyny, ściany), które mają utrudnić robotom przejazd.

Cukiereczek – robot autorstwa Mateusza Mroza (opisany na forum forbot)

Zawody MicroMouse – w których robot, czyli ”mysz” ma za zadanie w jak najkrótszym czasie rozwiązać labirynt. Klasyczny labirynt złożony jest z 256 (16×16) kwadratowych elementów o wymiarach 180×180 mm pooddzielanych między sobą ściankami. Rozwiązanie labiryntu polega na jego przeszukaniu i odnalezieniu najkrótszej ścieżki z kwadratu startowego (jest to jeden z narożników labiryntu) do celu (zazwyczaj usytuowanego na środku labiryntu). Wygrywa ta ”mysz”, która zrealizuje zadanie w najkrótszym czasie.

Nasze (moje i pana Marka Ogonowskiego) pierwsze podejście do Micromouse’ów (również opisane na forbocie)
Japońskie podejście do tematu

Zawody Sumo (oraz ich pochodne – MiniSumo, MicroSumo…) – w których roboty umieszczane na arenie zwanej dohyo (dodżo), ich celem jest zlokalizowanie, zaatakowanie i zepchnięcie z maty przeciwnika. Roboty nie mogą zawierać urządzeń zakłócających układu sterowania przeciwnika, błyskających świateł ani urządzeń blokujących ruch. W zależności od konkurencji limitowane są waga i wymiary konstrukcji (np. w MiniSumo waga nie może przekraczać 0,5 kg).

Walki potrafią być niezwykle szybkie

Zawody PuckCollect – w której roboty są umieszczane na planszy, na której porozrzucano niewielkie, kolorowe krążki (z ang. puck). Na początku każdego pojedynku, każdemu robotowi zostaje przydzielony kolor – czerwony lub niebieski, po czym roboty są umieszczane w bazach o odpowiadającym im kolorze. Celem jest dostarczenie do bazy jak najwięcej pucków wybranego koloru.

Najbardziej znany polski Puck Collect – Sarmatic (tutaj jego opis) – w akcji

I na koniec mniej znana, ale bardzo mi bliska kategoria – Ketchup House. Jest to bardzo nieprzewidywalna i widowiskowa konkurencja. Łączy w sobie cechy 4 wyżej opisanych kategorii rozgrywanych na tego typu imprezach. W każdej, trwającej 3 minuty rozgrywce udział biorą 2 roboty. Polem ich zmagań jest biała, kwadratowa plansza, na której znajduje się 10 czarnych linii (5 poziomych i 5 pionowych). Na skrzyżowaniach linii umieszczane są puszki z tytułowym keczupem.

Przykładowa puszka

Zadaniem robotów jest przemieszczenie puszek na swoja linie „domową”. Istnieje zupełna dowolność w sposobie przemieszczania i odstawiania puszek. Nie ma także ograniczeń co do sposobu poruszania sie po planszy – roboty mogą poruszać się po wyznaczonych liniach, ale nie musza. Przypadkowe zderzenia na ogół nie są karane, jednak niezgodne z zasadami jest zamierzone „dążenie do zderzenia” (np. wypychanie poza plansze). Roboty powinny wykrywać i omijać przeciwnika

Pomidor (również opisany na forbocie) – w którym maczałem palce 😀

Jak widać dyscypliny te są niezwykle różnorodne. Pozwalają na rozwijanie multidyscyplinarnych umiejętności – ze styku mechaniki, elektroniki, programowania czy analizy danych – to świetne wprowadzenie w każdą z tych dziedzin.

Logowanie pandas – pandas-log

TLDR: pandas-log umożliwia logowanie, przydatne zwłaszcza w długich łańcuchach operacji w pandas

Autorka zdjęcia: https://www.pexels.com/@rethaferguson

Pandas to wspaniała biblioteka, zwłaszcza dla osób, które miały doświadczenie w SQLu i chcą je wykorzystać, pracując z danymi w Pythonie. Pozwala na pracę na tabelarycznych danych w wygodny sposób.

Podobnie jednak jak w SQLu, czasami przychodzi moment, gdy podczas przetwarzania dzieje się coś dziwnego i nie do końca wiadomo, co poszło źle.

Załóżmy że pracujemy na tym samym datasecie graczy NBA z tego sezonu co w poprzednim wpisie i chcemy znaleźć drużynę, której pozycja centra jest najsilniej obsadzona.

import pandas as pd
from basketball_reference_web_scraper import client

players = pd.DataFrame(client.players_season_totals(season_end_year=2020))
players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

columns = ['made_field_goals', 'attempted_field_goals', 'made_three_point_field_goals',
       'attempted_three_point_field_goals', 'made_free_throws',
       'attempted_free_throws', 'offensive_rebounds', 'defensive_rebounds',
       'assists', 'steals', 'blocks', 'turnovers', 'personal_fouls', 'points']

for stat in columns:
  players[stat+"_per_game"] = players[stat] / players["games_played"]

for stat in columns:
  players[stat+"_per_36"] = players[stat] / players["minutes_played"] * 36.

Mając przygotowany dataset, chcemy przeprowadzić kilka operacji:
– wybrać graczy, którzy są centrami
– odfiltrować graczy, którzy rozegrali mniej niż 15 spotkań (gracze, ze „zbyt małą próbką”, aby coś o nich powiedzieć)
– wybrać 30 graczy z największą liczbą punktów na 36 minut
– policzyć w których zespołach znajdują się tacy zawodnicy
– uszeregować zespoły według ich liczby
Na podstawie tych kryteriów przygotowujemy sobie kod:

teams = players.copy()\
               .query("positions=='center'")\
               .query("games_played>15")\
               .nlargest(30, "points_per_36")\
               .groupby("team")["points_per_36"]\
               .count()\
               .sort_values(ascending = False)

i… okazuje się, że tak przygotowany kod zwraca nam pusty zbiór. Możemy oczywiście rozbijać i analizować każdą z operacji, ale możemy wykorzystać pakiet pandas-log, który nam w tym wydatnie pomoże

!pip install pandas_log
import pandas_log

with pandas_log.enable():
    teams = players.copy()\
                   .query("positions=='center'")\
                   .query("games_played>15")\
                   .nlargest(30, "points_per_36")\
                   .groupby("team")["points_per_36"]\
                   .count()\
                   .sort_values(ascending = False)

Wywołanie przygotowanego przez nas polecenia „z” pandas_log.enable() pokaże nam co się tam właściwie stało:

1) query(expr="positions=='center'", inplace=False):
	Metadata:
	* Removed 589 rows (100.0%), 0 rows remaining.
	Execution Stats:
	* Execution time: Step Took 0.006072 seconds.

2) query(expr="games_played>15", inplace=False):
	Metadata:
	* No change in number of rows of input df.
	Execution Stats:
	* Execution time: Step Took 0.005762 seconds.
	Tips:
	* Number of rows didn't change, if you are working on the entire dataset you can remove this operation.

3) nlargest(n=30, columns="points_per_36", keep='first'):
	Metadata:
	* Picked 30 largest rows by columns (points_per_36).
	Execution Stats:
	* Execution time: Step Took 0.008734 seconds.
	Tips:
	* Number of rows didn't change, if you are working on the entire dataset you can remove this operation.

4) groupby(by="team", axis=0, level=None, as_index:bool=True, sort:bool=True, group_keys:bool=True, squeeze:bool=False, observed:bool=False):
	Metadata:
	* Grouping by team resulted in 0 groups like ,
	  and more.
	Execution Stats:
	* Execution time: Step Took 0.001026 seconds.

Jak widać, już w pierwszym poleceniu było coś nie tak (0 rows remaining). Po dokładniejszym przyjrzeniu się okazuje się, że wpisaliśmy nazwę pozycji używając małych liter.

Poprawienie tego błędu:

with pandas_log.enable():
    teams = players.copy()\
                   .query("positions=='CENTER'")\
                   .query("games_played>15")\
                   .nlargest(30, "points_per_36")\
                   .groupby("team")["points_per_36"]\
                   .count()\
                   .sort_values(ascending = False)

co w logach wygląda to już na poprawne:

1) query(expr="positions=='CENTER'", inplace=False):
	Metadata:
	* Removed 485 rows (82.34295415959252%), 104 rows remaining.
	Execution Stats:
	* Execution time: Step Took 0.007955 seconds.

2) query(expr="games_played>15", inplace=False):
	Metadata:
	* Removed 18 rows (17.307692307692307%), 86 rows remaining.
	Execution Stats:
	* Execution time: Step Took 0.007225 seconds.

3) nlargest(n=30, columns="points_per_36", keep='first'):
	Metadata:
	* Picked 30 largest rows by columns (points_per_36).
	Execution Stats:
	* Execution time: Step Took 0.006591 seconds.

4) groupby(by="team", axis=0, level=None, as_index:bool=True, sort:bool=True, group_keys:bool=True, squeeze:bool=False, observed:bool=False):
	Metadata:
	* Grouping by team resulted in 20 groups like 
		ATLANTA HAWKS,
		BOSTON CELTICS,
		CHARLOTTE HORNETS,
		DALLAS MAVERICKS,
		DENVER NUGGETS,
	  and more.
	Execution Stats:
	* Execution time: Step Took 0.001055 seconds.

Możemy więc zobaczyć upragnioną listę:

team count
0 PHOENIX SUNS 4
1 MINNESOTA TIMBERWOLVES 3
2 NEW ORLEANS PELICANS 2
3 CHARLOTTE HORNETS 2
4 LOS ANGELES CLIPPERS 2
5 MEMPHIS GRIZZLIES 2
6 WASHINGTON WIZARDS 2
7 ORLANDO MAGIC 1
8 NEW YORK KNICKS 1
9 PHILADELPHIA 76ERS 1
10 UTAH JAZZ 1
11 PORTLAND TRAIL BLAZERS 1
12 SAN ANTONIO SPURS 1
13 INDIANA PACERS 1
14 DETROIT PISTONS 1
15 DENVER NUGGETS 1
16 DALLAS MAVERICKS 1
17 TORONTO RAPTORS 1
18 BOSTON CELTICS 1
19 ATLANTA HAWKS 1

Szybka analiza eksploracyjna – pandas-profiling

TLDR: pandas-profiling to bardzo wygodne narzędzie do przeprowadzenia szybkiej analizy eksploracyjnej – obszerny raport można uzyskać za pomocą 2 linijek

Bardzo często spotykamy się po raz pierwszy z nieznanym datasetem. Chcąc podejrzeć co się w nim znajduje mamy kilka opcji. Najbardziej oczywistą wydaje się (po wczytaniu do pandasa) wykonanie na nim metody head() lub tail(). Ale to daje nam tylko pobieżną informację o kilku pierwszych (bądź ostatnich) rekordach.

Jeżeli chcemy głębiej zajrzeć w dane, z pomocą może nam przyjść pakiet pandas-profiling, który całą czarną robotę wykona za nas.

Ponieważ wielkimi krokami zbliżają się play-offy w lidze NBA możemy spróbować poczuć się jak autor profilu „O Futbolu Statystycznie” i spróbować pogrzebać w statystykach najlepszej ligi koszykarskiej. Morze statystyk można znaleźć na wspaniałej stronie basketball referrence. Tak się składa, że pewien autor stworzył swoiste pythonowe api do tej strony – pakiet basketball_reference_web_scraper .

!pip install --upgrade --quiet pandas_profiling
!pip install --quiet basketball_reference_web_scraper

import numpy as np
import pandas as pd
from pandas_profiling import ProfileReport
from basketball_reference_web_scraper import client

Po zainstalowaniu i zaimportowaniu niezbędnych pakietów można brać się do faktycznej analizy – za swój cel obierzemy graczy, którzy grali w bieżącym sezonie (2019/20). Zbiór wymaga małego oczyszczenia w polach dotyczących pozycji oraz klubu.

players = pd.DataFrame(client.players_season_totals(season_end_year=2020))
players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

Aby móc lepiej porównywać dokonania zawodników dodamy statystyki uśrednione liczbą meczów oraz uśrednione do 36 minut.

columns = ['made_field_goals', 'attempted_field_goals', 'made_three_point_field_goals',
       'attempted_three_point_field_goals', 'made_free_throws',
       'attempted_free_throws', 'offensive_rebounds', 'defensive_rebounds',
       'assists', 'steals', 'blocks', 'turnovers', 'personal_fouls', 'points']

for stat in columns:
  players[stat+"_per_game"] = players[stat] / players["games_played"]

for stat in columns:
  players[stat+"_per_36"] = players[stat] / players["minutes_played"] * 36.

Z tak przygotowanym datasetem możemy rozpocząć naszą analizę. Utworzenie raportu jest banalnie proste – wystraczy utworzyć obiekt zawierający „podsumowanie”:

profile = ProfileReport(players, title='NBA Players Profiling Report')

A następnie wyświetlić bądź zapisać:

profile.to_widgets() # wyświetlenie w postaci osadzonego "widżetu"
profile.to_notebook_iframe() # wyświetlenie w postaci osadzonego kodu html
profile.to_file("nba_players.html") # zapis do pliku html

Raport składa się z kilku części. W pierwszej z nich znajdują się najbardziej podstawowe informacje o datasecie – liczba (i rodzaj) kolumn, liczba rekordów, informacja o rekordach pustych i zduplikowanych:

Kolejną część stanowią statystyki dotyczące poszczególnych kolumn datasetu – w przypadku zmiennych kategorycznych, dotyczą one m.in. liczby i liczności poszczególnych kategorii oraz liczby braków danych

Pozwoliło mi to np. na zauważenie, że dla zawodników, którzy zmieniali klub, występuje po kilka rekordów – po jednym dla każdego klubu w którym zagrali chociaż miutę.

W przypadku zmiennych liczbowych informacji jest jeszcze więcej – m.in. rozkład zmiennej, liczba zer, minimalne i maksymalne wartości

Dodatkowe informacje można znaleźć odsłaniając ukrytą zawartość (przycisk toggle details).

Dzięki temu można zobaczyć jak odstającym przypadkiem jest Vince Carter

Ponadto generowana jest macierz korelacji pomiędzy zmiennnymi

Co ciekawe, wiek nie jest silnie skorelowany z żadną ze zmiennych

Jednak zdecydowanie najlepszym efektem jest interaktywny wykres pozwalający na sprawdzenie interakcji pomiędzy dowolnymi dwoma zmiennymi. Dla przykładu to jak wyglądają średnie punktów i asyst w meczu

Widać bardzo dobrze outliery w osobach Jamesa Hardena, Trae Younga i LeBrona Jamesa (3 punkty w prawym górnym rogu)

Niestety – to rozwiązanie ma jednak swoje minusy – wygenerowany plik (przy dużej liczbie zmiennych) może dużo ważyć – dla tego datasetu (50 kolumn) plik z analizą w postaci html-a waży ponad 150 MB

Kod można (jak zawsze) podejrzeć na gitlabie

Prawo Goodharta (2/2)

TLDR: Kiedy miara staje się „celem”, przestaje być dobrą miarą.

Autor zdjęcia: https://www.pexels.com/@expect-best-79873

Zjawisko to można było zaobserwować historycznie (co poruszyłem w napisanej jakiś czas temu pierwszej części). Prawo to bierze nazwę od Charlesa Goodharta – brytyjskiego ekonomisty.

W oryginale brzmiało ono: „Każda obserwowana statystycznie zależność ma skłonność do zawodzenia, w momencie w którym zaczyna być wykorzystywana do celów regulacyjnych” i dotyczyła prowadzenia brytyjskiej polityki monetarnej. Goodhart zauważył, że próba wprowadzenia ograniczeń na banki komercyjne przez bank centralny nie przyniesie zamierzonego efektu, ponieważ znajdą one sposób na świadczenie danej usługi pod inną nazwą oraz w inny sposób. Aby zoptymalizować swoje zyski „skalibrują się” do wybranego kryterium.

Podobne zjawisko można zauważyć w przypadku stron internetowych. Problemem przed którym stanęli producenci wyszukiwarek było stworzenie algorytmu pozycjonowania – w Google początkowo był to PageRank (o którego dokładniejszych założeniach i implementacji można przeczytać bardzo ciekawy artykuł na Geeks for Geeks). W miarę rozwoju tych algorytmów bardzo szybko pojawiły się firmy zajmujące się pozycjonowaniem stron w internecie. Za opłatą wykorzystywały one logikę działania tych metod (które premiowały m.in. występowanie słów kluczowych oraz hiperłączy prowadzących do strony) po to, aby premiować strony w wynikach wyszukiwania. Powoduje to sytuację, w której najwyżej nie są „najlepsze” wyniki, a jedynie te „najlepiej spozycjonowane”. Z problemem nie udało się do tej pory skutecznie wygrać – przykład może stanowić próba sprawdzenia czy najbliższa niedziela jest handlowa, czy nie:

Bardzo często efekt ten można zaobserwować także w korporacjach. W momencie gdy premie są przyznawane za spełnienie danego kryterium, mogą wydarzyć się niekorzystne z punktu widzenia całej firmy, ale optymalne dla danego działu sytuacje. Dla przykładu – dział odpowiedzialny za windykację jest rozliczany jedynie za liczbę dłużników, których przekonali do spłaty. Może to doprowadzić do sytuacji, w której nie będą przypominać klientom o dacie spłaty, przez co wygenerują sobie dużo „łatwych” punktów premiowych – klientów, którzy zamierzali zapłacić, a jedynie o tym zapomnieli. Dla firmy koszt wzrośnie (dział windykacji będzie miał więcej przypadków, więc nie będzie miał czasu na częstsze napastowanie rzeczywistych dłużników), pomimo tego, że kryterium będzie wyglądać coraz lepiej.

W świecie finansowym pewnym problemem jest także kwestia „uczenia się” przez klientów tego co może być składnikiem skoringu kredytowego. W związku z tym klienci mogą podejmować próby oszukiwania na wniosku (np. zatajania rozdzielności majątkowej albo liczby dzieci na utrzymaniu), bądź nawet podejmować akcje, które mają w ich mniemaniu podnieść ich wiarygodność w oczach banku – np. brać małe niepotrzebne pożyczki, by „stworzyć dobrą historię kredytową” albo dostawać dodatkowe przelewy od znajomych, tak aby wytworzyć iluzję wyższego osiąganego dochodu. Działanie to powoduje, że dane kryterium przestaje być tak dobre, jak powinno według danych historycznych.

W przypadku bezzałogowych statków powietrznych, ważnym kryterium które dla wielu osób czy instytucji decyduje o zakupie danego produktu jest jego zasięg sterowania – to jak faktycznie daleko „dron” może odlecieć. W związku z tym niektórzy producenci stosują bardzo kierunkowe anteny – które zapewniają duży zasięg w jednym kierunku, ale „nie radzą sobie” zupełnie w innym. W skrajnych przypadkach może to powodować, że kopter będzie miał tendencję do zrywania połączenia po nawet lekkim obróceniu się. Innym przykładem z tej działki jest manipulowanie długotrwałością – czasem pozostawania w powietrzu. Można stworzyć konstrukcję, która przy bezwietrznej pogodzie, bez payloadu (akcesorium faktycznie wykonującego zadanie – np. kamery) będzie pozostawać w powietrzu bardzo długo (co zostanie skrupulatnie zaznaczone na pudełku). Ze względu na zbyt mały zapas mocy czas skróci się jednak dramatycznie, jeśli zamocujemy tam kamerę i faktycznie będziemy chcieli coś nakręcić.

Jak na złość – oba problemy.

W uczeniu maszynowym prawo to także potrafi zmaterializować się, czasami w nieoczywisty sposób. Pierwszy, najbardziej rzucający się w oczy, to kwestia przeuczania modelu. Jeżeli nasz model jest zbyt skomplikowany w stosunku do liczby posiadanych danych, może się zdarzyć, zastosowanie kryterium jakości klasyfikacji na zbiorze treningowym może skutkować jego przeuczeniem. Model „zapamięta” wtedy kombinacje – zamiast uogólnić rozwiązanie.

Czarna linia – sensowny podział. Zielona – przeuczenie.

Innym, bardziej subtelnym i trudniejszym do wychwycenia problemem jest sama kwestia definicji sukcesu i doboru próbki treningowej. Załóżmy, że pracujemy w firmie telekomunikacyjnej i chcemy stworzyć model, który będzie odrzucał klientów z wysokim prawdopodobieństwem powstania zadłużenia. Jeżeli wybierzemy jako definicję „zły klient to taki, który w ciągu roku od podpisania umowy przynajmniej raz spóźnił się z fakturą”, to model nauczony na takiej definicji może znakomicie wyglądać na papierze. Zbiór prawdopodobnie będzie lepiej zbilansowany, co może przełożyć się na jego wyniki w kryteriach jakości. Jednak ze względu na to że znakomita większość opóźnień w fakturach to krótkie opóźnienia, to model może nauczyć się cech charakteryzujących ludzi, mających skłonność do takich zachowań (np. młody wiek, korzystanie z telefonu do wykonywania opłat), a nie cech charakteryzujących „zatwardziałych dłużników”. W związku z tym możemy pozbyć się z populacji części osób, które generują zysk, a nie tych, którzy powodują straty.

Innym przykładem może być zagadnienie dość szeroko opisywane w branżowych portalach – model który miał rozróżniać wilki od psów rasy husky, pomimo tego, że dawał sobie świetnie radę na zbiorach treningowych i testowych nie działał zupełnie w rzeczywistym środowisku. Po długiej analizie wag modelu, rozwiązanie tej zagadki okazało się zaskakująco proste. Wszystkie zdjęcia wilków były wykonane w czasie jednej sesji w czasie zimy, podczas gdy zdjęcia psów pochodziły z internetu i były wykonywane w różnych porach roku i różnych miejscach. Model nauczył się zatem (z dużą dokładnością) wykrywać śnieg. (więcej na ten temat tutaj)

Reasumując – pomimo tego, że „uczenie maszynowe” brzmi bardzo górnolotnie, a metody za nim stojące są czasami wyjątkowo wysublimowane, to nadal jest to jedynie algorytm minimalizujący jakieś kryterium, bez głębszego rozumienia problemu.