import numpy as np
import scipy
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
%matplotlib inline
Няма да влизаме във всичко. Няма дори да чертаем пълна картинка – петте библиотеки са огромни и е добре да разгледате всяка от тях за детайли.
Най-важното нещо в NumPy е np.array
, което е многомерен масив (тензор) с който може да извършваме стандартните операции.
array = np.array([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]])
array
Масивите имат следните атрибути:
ndim
– брой измеренияshape
– "кортеж" с размерности на всяко измерениеsize
– брой елементиdtype
– тип на елементите (всички елементи са от един тип)itemsize
– брой байтове за един елементarray.ndim
array.shape
array.size
array.dtype
array.itemsize
array.dtype
NumPy ползва библиотеки на Fortran и C++ за да прави ефективни изчисления. По тази причина обектите му са по-ограничени от стандартни питонски масиви, но пък много по-ефективни.
Редове могат да се вземат с индексация:
array[0]
array[-1]
Елементи също:
array[1, 2]
Ако искате да вземете цяло измерение, може да ползвате slice (както и празен такъв):
array[1, :]
array[:, 1]
array[:3, 1:]
Може да промените размера с reshape
.
array.reshape(4, 3)
array.reshape(2, 6)
array.reshape(12)
Аргумент от -1
определя неясно измерение:
array.reshape(1, -1)
array.reshape(-1, 6)
array.reshape(-1, 1)
Има ones
и zeros
, които инициализират масив с определени размери.
np.ones(4)
np.zeros((4, 4))
np.ones((2, 3, 4))
Има и още няколко подобни:
Единична матрица.
np.eye(4)
Диагонална матрица:
np.diag([1, 2, 3, 4])
Матрица с произволни стойности в $[0, 1)$:
np.random.rand(3, 4)
NumPy има няколко типа – може да ги определяте при конструкция:
np.ones((3, 4), dtype=np.bool)
np.ones((3, 4), dtype=np.int8)
np.ones((3, 4), dtype=np.float128)
Може да мислите за елементите като матрици, но трябва да знаете, че операциите с тях се прилагат elementwise:
i = np.eye(4)
i
random_numbers = np.random.rand(4, 4)
random_numbers
Произведението на тези две матрици трябва да даде random_numbers
(i
е единична матрица). Резултата, обаче, е друг:
np.eye(4) * np.random.rand(4, 4)
Събирането е същото:
i + random_numbers
Както и повдигането на степен:
random_numbers ** i
Ако единия операнд е скалар, операцията се "векторизира" – изпълнява се за всеки елемент:
random_numbers + 1
(random_numbers * 10).astype(np.int64)
random_numbers - 0.5
Резултата може и да е друг тип:
random_numbers >= 0.5
Може да индексираме с масиви и да вземем стойностите от няколко места:
numbers = (np.random.rand(12) * 100).astype(np.int64)
numbers
print("numbers[2] = {}".format(numbers[2]))
print("numbers[4] = {}".format(numbers[4]))
print("numbers[6] = {}".format(numbers[6]))
numbers[[2, 4, 6]]
Може да индексираме и с булев масив:
random_numbers >= 0.5
random_numbers[random_numbers >= 0.5]
Като нещо допълнително, ако искаме да оставим числата по-малки от 0.5 да са нули, може да го направим така:
random_numbers * (random_numbers >= 0.5)
Това работи, защото True
се брои за 1
, а False
за 0
.
Ако единия операнд има по-малко измерения, операцията се прилага многократно за всеки елемент от другите измерения:
random_numbers + np.array([1, 2, 3, 4])
random_numbers + np.array([[1], [2], [3], [4]])
Може да "конкатенираме" масиви:
random_numbers = ((np.random.rand(3, 4) * 100) + 1).astype(np.int64)
random_numbers
np.hstack([random_numbers, np.eye(3, dtype=np.int64)])
np.vstack([random_numbers, np.diag([1, 2, 3, 4])])
ravel
"разплита" масив:
tensor = np.random.rand(2, 3, 4)
tensor
tensor.ravel()
np.arange
е аналогичен на range
в Python:
np.arange(10, 30, 5)
np.linspace
връща точки в линейно пространство:
np.linspace(10, 20, 5)
np.linspace(0, 10)
Има и доста функции:
sum
mean
median
max
min
Те приемат аргумент за измерение, по което да работят.
random_numbers
random_numbers.sum()
random_numbers.sum(axis=0)
random_numbers.sum(axis=1)
random_numbers.sum(0)
random_numbers.max()
random_numbers.max(1)
random_numbers.mean()
random_numbers.mean(0)
NumPy предлага и функции, които се изпълняват elementwise върху масива.
numbers = np.random.rand(3, 3) * 3 + 1
numbers
np.exp(numbers)
np.log(numbers)
np.sin(numbers)
Прилагането на функция на всеки елемент е малко тегаво:
def plus_one_square(x):
return x**2 + 2 * x + 1
plus_one_square(3)
Има декоратор, np.vectorize
, който прави функцията работеща:
vectorized = np.vectorize(plus_one_square)
vectorized(numbers)
Разбира се, това е бавно. Ако можете, по-добре да си разпишете сметката:
numbers**2 + 2 * numbers + 1
Или, донякъде изненадващо:
plus_one_square(numbers)
Duck Typing FTW!
Има и разни познати неща от линейната алгебра.
matrix = np.arange(1, 10).reshape(3, 3)
matrix
matrix.transpose()
matrix.T
В Python (баси тъпия език) има оператор за умножение на матрици – @
(сериозно!):
matrix @ np.eye(3)
np.diag([1, 2, 3]) @ matrix
Може и да обръщате матрица:
matrix = np.random.rand(3, 3)
matrix
inverse = np.linalg.inv(matrix)
inverse
matrix @ inverse
(matrix @ inverse).round(2)
Разбира се, (1) тази операция не винаги минава и (2) е супер бавна.
В NumPy има още доста. Най-добре прегледайте документацията набързо:
SciPy е огромен. Ето само някои от нещата, които има:
scipy.cluster
)scipy.constants
)scipy.fftpack
)scipy.integrate
)scipy.interpolate
)scipy.io
)scipy.linalg
)scipy.misc
)scipy.ndimage
)scipy.odr
)scipy.optimize
)scipy.signal
)scipy.sparse
)scipy.sparse.linalg
)scipy.sparse.csgraph
)scipy.spatial
)scipy.special
)scipy.stats
)scipy.stats.mstats
)scipy.optimize.minimize(lambda x: x**2 + 2 * x + 1, 0.7123412)
Как? Сложно.
Като друго забавление, нека видим интеграцията:
from matplotlib.patches import Polygon
def draw_sine_integration_example():
func = np.sin
a, b = np.pi / 2, np.pi
x = np.linspace(-0.2, np.pi * 1.5)
y = func(x)
fig, ax = plt.subplots(figsize=(10, 6))
plt.plot(x, y, 'r', linewidth=2)
plt.grid()
ix = np.linspace(a, b)
iy = func(ix)
verts = [(a, 0)] + list(zip(ix, iy)) + [(b, 0)]
poly = Polygon(verts, facecolor='0.9', edgecolor='0.5')
ax.add_patch(poly)
ax.axhline(color='k')
ax.axvline(color='k')
ax.annotate(r'$\frac{\pi}{2}$', (np.pi / 2, 0), xytext=(0, -20), textcoords='offset points', size='x-large')
ax.annotate(r'$\pi$', (np.pi, 0), xytext=(0, -20), textcoords='offset points', size='x-large')
draw_sine_integration_example()
$$ \begin{align*} \int_{\pi / 2}^{\pi}\sin(x)\mathrm{d} x &= - \cos(x) \Big|_{\pi / 2}^\pi \\ &= - \cos(\pi) - \big(- \cos(\frac{\pi}{2}) \big) \\ &= \cos(\frac{\pi}{2}) - \cos(\pi) \\ &= 0 - (-1) \\ &= 1 \end{align*} $$
scipy.integrate.quad(lambda x: np.sin(x), np.pi / 2, np.pi)
pandas
е data science библиотека за Python. Дава ни два основни типа – pd.Series
и pd.DataFrame
.
pd.Series
е едноизмерен масив от стойностиpd.DataFrame
е таблица от данни. Може да мислите за нея като масив от pd.Series
, където последните са всяка колона.Ще разгледаме първо pd.Series
.
series = pd.Series([2, 4, -1, 4])
series
Може да вземете стойностите като NumPy масив:
series.values
Всеки Series
има и иднекс:
series.index
Индекса може да има произволни стойности:
series = pd.Series([2, 4, -1, 4], index=['a', 'b', 'c', 'd'])
series
series.index
Индексирането работи както очакваме:
series['a']
series['e'] = -12
series
series[['a', 'e', 'c']]
Има и позната векторизация от NumPy:
series * 2
series > 0
Както и индексиране с булев series. Обърнете внимание на съвпадащите и запазващите се индекси:
series[series > 0]
Може да прекарете Series
през универсална функция:
np.log(series * series)
Може да конструирате Series
с речник, където ключовете са индекса, а стойностите са... стойностите.
capitals = {'Bulgaria': 'Sofia', 'United Kingdom': 'London', 'Malaysia': None}
series = pd.Series(capitals)
series
Има isnull
series.isnull()
Индекса и редицата могат да имат имена:
series.name = 'capital'
series.index.name = 'country'
series
Това понякога е полезно за да се създадат ясни данни.
Има и основните статистически операции:
series = pd.Series([1, 5, 3, 2, 9, 1, 13, 5, 1, 5])
series.mean()
series.median()
series.mode()
series.std()
series.var()
DataFrame-а е таблица от данни, в която всяка колона има собствен тип.
Един начин да го конструираме е с речник, където всяка двойка ключ-стойност е колона.
pd.DataFrame({
'Name': ['John Snow', 'Arya Stark', 'Daenerys Targeryan', 'Jamie Lannister'],
'Gender': pd.Categorical(['male', 'female', 'female', 'male']),
'Age': [16, 11, 16, 36],
'Brothers': [3, 4, 2, 1],
'Sisters': [2, 1, 1, 1],
}, columns=['Name', 'Gender', 'Age', 'Brothers', 'Sisters'])
pd.Categorical
е нещо като pd.Series
, но с по-оптимален запис в паметта.
pd.Categorical(['male', 'female', 'female', 'male'])
Може да дадем данните и таблично, като определим имената на колоните:
data = [
['Eddard Stark', 34, 41, 'Stark', 'male'],
['Catelyn Stark', 33, 40, 'Stark', 'female'],
['Daenerys Targaryen', 13, 16, 'Targaryen', 'female'],
['Tyrion Lannister', 24, 32, 'Lannister', 'male'],
['Jon Snow', 14, 16, 'Stark', 'male'],
['Brandon Stark', 7, 10, 'Stark', 'male'],
['Sansa Stark', 11, 13, 'Stark', 'female'],
['Arya Stark', 9, 11, 'Stark', 'female'],
['Theon Greyjoy', 18, 16, 'Greyjoy', 'male'],
['Davos Seaworth', 37, 49, 'Seaworth', 'male'],
['Jaime Lannister', 31, 36, 'Lannister', 'male'],
['Samwell Tarly', 14, 17, 'Tarly', 'male'],
['Cersei Lannister', 31, 36, 'Lannister', 'female'],
['Brienne of Tarth', 17, 32, 'Tarth', 'female']
]
characters = pd.DataFrame(data, columns=['name', 'book_age', 'tv_age', 'house', 'gender'])
characters
Индексирането взема колони (като series):
characters['name']
characters.name
characters[['name', 'tv_age']]
Ако искаме да индексираме по редове, трябва да ползваме loc
:
characters.loc[3]
Обърнете внимание, че резултате е Series
от реда. Индекса е имената на колоните, а типа е най-генералния (object
).
Може да вземете slice:
characters.loc[1:5]
Може да комбинираме редове и колони с loc
:
characters.loc[1:5, ['name', 'gender']]
Бихме могли дори да преименуваме индекса:
characters.index = ['ned', 'cat', 'dany', 'tyrion', 'jon', 'bran', 'sansa', 'arya', 'theon', 'davos', 'jaime',
'sam', 'cersei', 'brienne']
characters
Тогава индексацията става другояче:
subset = ['ned', 'cat', 'jaime']
columns = ['name', 'house', 'gender']
characters.loc[subset, columns]
Може да правите стандартната векторна математика с колоните:
differences = characters.tv_age - characters.book_age
differences
Виждаме, че средно персонажите са по-възрастни в сериала:
differences.mean()
differences.median()
Може да добавим колоната в DataFrame-а. Отново, обърнете внимание на съвпадащите индекси:
characters['age_difference'] = characters.tv_age - characters.book_age
characters
Ако добавим нова колона с разминаващ се индекс, останалите стойности ще се попълнят като празни. Липсващите индекси ще се игнорират:
alive = pd.Series([False, True, False], index=['ned', 'dany', 'viserys'])
characters['alive'] = alive
characters
Бележка: спойлерите са от първи сезон. Ако не сте го гледали – къде живеете?
Друга модификация:
characters['underage_tv'] = characters.tv_age < 18
characters
Може да попълним липсващите стойности с fillna
. Това вече сме го виждали:
characters.alive = characters.alive.fillna(True)
characters
Spoiler alet: не всички са живи
Може да вземем и NumPy матрицата:
characters.values
Може да махате колони и редове с drop
:
characters.drop(['ned', 'cat', 'sansa', 'arya', 'bran'])
characters.drop(['tv_age', 'book_age', 'age_difference'], axis=1)
И разбира се, индексацията също върши работа за това:
characters.house == 'Stark'
~(characters.house == 'Stark')
characters[~(characters.house == 'Stark')]
Има и rank
, който връща ранка при сортиране. method
взема няколко стойности, които определят какво да стане със елементите, които са еднакви:
characters.tv_age.rank(method='min')
characters['age_rank'] = characters.tv_age.rank(method='min')
characters
characters.sort_values(by='age_rank')
characters.sum()
characters.sum(axis=1)
characters.describe()
characters.tv_age.value_counts()
Поддържат се и йерархични индекси:
data = pd.Series(np.random.randn(9),
index=[['a', 'a', 'a', 'b', 'b', 'c', 'c', 'd', 'd'],
[1, 2, 3, 1, 3, 1, 2, 2, 3]])
data
Индекс обекта е малко по-особен:
data.index
Индексиране на редица с йерархичен индекс връща редицата на вложения индекс:
data['b']
unstack
обръща йерархичния индекс до измерение на таблицата:
data.unstack()
stack
е обратната операция:
data.unstack().stack()
DataFrame
-а също може да има йерархични индекси по всички измерения:
frame = pd.DataFrame(np.arange(12).reshape((4, 3)),
index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
columns=[['Ohio', 'Ohio', 'Colorado'],
['Green', 'Red', 'Green']])
frame
Индексацията на колона връща друг DataFrame
:
frame['Ohio']
Може да дадем имена на индексите:
frame.index.names = ['key1', 'key2']
frame.columns.names = ['state', 'color']
frame
swaplevel
ни позволява да разменим реда на индексите:
frame.swaplevel('key1', 'key2')
Ако сортираме след това, нещата ще изглеждат както очакваме:
frame.swaplevel('key1', 'key2').sort_index(level=0)
Има и операции, подобни на join-овете в базите от данни:
df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],
'data1': range(7)})
df2 = pd.DataFrame({'key': ['a', 'b', 'd'],
'data2': range(3)})
df1
df2
merge прави стандартен join (декартово произведение на редовете):
pd.merge(df1, df2)
По подразбиране се ползват съвпадащите колони. Може да го направим експлицитно:
pd.merge(df1, df2, on=['key'])
По подразбиране се прави inner join – ако някой ключ го няма в един от двата DataFrame
-а, няма да го има и в резултата. outer join-а запази реда, попълвайки стойностите с NaN
:
pd.merge(df1, df2, how='outer')
Следва кратък и непълен пример в matplotlib.
plot
чертае функция:
plt.figure(figsize=(10, 6))
data = np.arange(10, 20)
plt.plot(data);
plot
приема два аргумента – $x$ и $f(x)$:
xs = np.arange(10)
ys = 2 ** xs
xs
ys
plt.figure(figsize=(12, 8))
plt.plot(xs, ys);
Тая функция е малко ръбата, затова е по-добре да ползваме linspace
:
xs = np.linspace(0, 10, 100)
ys = 2 ** xs
plt.figure(figsize=(12, 8))
plt.plot(xs, ys);
scatter
пък рисува точки:
points = np.random.rand(20, 2)
plt.figure(figsize=(12, 8))
plt.scatter(points[:, 0], points[:, 1]);
points
points[:, 0]
Настройките по подразбиране могат да се променят с rc
:
plt.rc('figure', figsize=(12, 8))
plt.scatter(points[:, 0], points[:, 1], color="red");
Повечето може да подавате цветове и стил:
plt.scatter(points[:, 0], points[:, 1], marker='^', color='green', s=200);
Има и къс запис:
xs = np.linspace(0, 10, 30)
ys = 2 ** xs
plt.plot(xs, ys, 'ko--');
Може да нарисувате много неща наведнъж. При %matplotlib inline
всички неща от една клетка ще се начертаят в една фигура.
a = np.random.rand(10, 2)
b = np.random.rand(10, 2)
c = np.random.rand(10, 2)
plt.scatter(a[:, 0], a[:, 1], marker='+', c='g', s=100)
plt.scatter(b[:, 0], b[:, 1], marker='*', c='r', s=150)
plt.scatter(c[:, 0], c[:, 1], marker='o', c='c', s=50)
plt.grid();
Може да чертаем и хистограми:
plt.hist(np.random.randn(3000), bins=40, color='k', alpha=0.3);
Може да съпоставим и хистограмата с едно нормално разпределение:
xs = np.linspace(-5, 5, 200)
ys = (1 / np.sqrt(2)) * np.exp(- xs ** 2 / 2)
plt.hist(np.random.randn(3000), bins=40, color='k', alpha=0.3)
plt.plot(xs, ys * 300);
Една фигура може да съдържа други subplot-ове:
fig = plt.figure()
ax1 = fig.add_subplot(2, 2, 1)
ax2 = fig.add_subplot(2, 2, 2)
ax3 = fig.add_subplot(2, 2, 3)
Първите два аргумента (2, 2
) са колко subplot-а да има хоризонтално и вертикално. Третия е индекса, който искаме да създадем:
fig = plt.figure()
ax1 = fig.add_subplot(2, 2, 1)
ax2 = fig.add_subplot(2, 2, 2)
ax3 = fig.add_subplot(2, 2, 3)
ax1.hist(np.random.randn(100), bins=20, color='r', alpha=0.5)
ax2.scatter(np.arange(30), np.arange(30) + 3 * np.random.randn(30))
ax3.plot(np.random.randn(30).round().cumsum(), 'k-', drawstyle='steps-post');
За последно, нека видим и примера с интеграцията от по-рано:
func = np.sin
a, b = np.pi / 2, np.pi
x = np.linspace(-0.2, np.pi * 1.5 + 0.2)
y = func(x)
fig, ax = plt.subplots(figsize=(10, 6))
plt.plot(x, y, 'r', linewidth=2)
plt.grid()
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')
ax.xaxis.set_ticks(np.arange(0, 5, np.pi / 4))
ix = np.linspace(a, b)
iy = func(ix)
verts = [(a, 0)] + list(zip(ix, iy)) + [(b, 0)]
poly = Polygon(verts, facecolor='0.9', edgecolor='0.5')
ax.add_patch(poly)
ax.annotate(r'$\frac{\pi}{2}$', (np.pi / 2, 0), xytext=(0, -20), textcoords='offset points', size='x-large')
ax.annotate(r'$\pi$', (np.pi, 0), xytext=(0, -20), textcoords='offset points', size='x-large');
От тук нататък, най-добрия начин да научите повече за matplotlib е да гледате примери и да ровите из документацията. Обикновено имате конкретен проблем, който искате да решите, и това работи най-добре.
Освен ако не искате да налеете 20-40 часа да разберете всичко в детайли, което също не е лошо.
Тази библиотека дава интерфейс от по-високо ниво за чертане на диаграми. Също, има интеграция с pandas.
x = np.random.normal(size=200)
sns.distplot(x);
Обърнете внимание как сами си избра bins и си начерта дистрибуцията с крива.
mean, cov = [0, 1], [(1, .5), (.5, 1)]
data = np.random.multivariate_normal(mean, cov, 200)
df = pd.DataFrame(data, columns=["x", "y"])
df.head()
sns.jointplot(x="x", y="y", data=df, size=12);
Seaborn има собствени идеи за добре изглеждащи диаграми. Може да ги включите така:
sns.set()
Вече нещата ще изглеждат малко по-различно:
sns.jointplot(x="x", y="y", data=df, size=12);
Pairplot-а е много удобен начин да разглеждате зависимости между feature-и. За това е подходящ един от стандартните dataset-и, iris. Има го в sklearn, но ще го вземем през seaborn, където е в по-добра форма:
iris = sns.load_dataset("iris")
iris.sample(20)
Това са категориите:
iris.species.value_counts()
Ето това са feature-ите:
sns.pairplot(iris, hue="species");
Да вземем два други dataset-а от seaborn:
titanic = sns.load_dataset("titanic")
tips = sns.load_dataset("tips")
Единия вече го познавате:
titanic.head(20)
Другия е за бакшиши:
tips.head(20)
Има boxplot, който показва дистрибуция и стандартно отклонение:
sns.boxplot(x="day", y="total_bill", hue="time", data=tips);
barplot е ясен:
sns.barplot(x="sex", y="survived", hue="class", data=titanic);
regplot
чертае линейни зависимости:
sns.regplot(x="total_bill", y="tip", data=tips);
Повече детайли има в tutorial-а.
Финално, pandas
има няколко метода, които посягат директно към seaborn.
df = pd.DataFrame(np.random.randn(10, 4).cumsum(0),
columns=['A', 'B', 'C', 'D'],
index=np.arange(0, 100, 10))
df
df.plot();
df.hist();
df.plot.bar();
df.plot.barh(stacked=True);
Това може да се комбинира със ванила matplotlib:
fig, axes = plt.subplots(2, 2, figsize=(16, 10))
axes = axes.ravel()
df.plot.bar(ax=axes[0])
sns.distplot(df.A, ax=axes[1])
axes[2].plot(np.linspace(-3, 3, 10), -np.linspace(-3, 3, 10) ** 2)
df.plot.barh(stacked=True, ax=axes[3]);
Има още доста:
Четете си отделно.