Apache Spark — это платформа, позволяющая быстро обрабатывать большие объемы данных.
Предварительная обработка данных является необходимым шагом в машинном обучении, поскольку качество данных влияет на результат и производительность модели машинного обучения, которую мы применили к данным. Поэтому чрезвычайно важно, чтобы мы предварительно обработали наши данные, прежде чем вводить их в нашу модель.
Задача предварительной обработки данных также выполняется пандами и Pyspark. Здесь мы собираемся изучить Pyspark.
Прежде чем мы углубимся в часть предварительной обработки, давайте сначала кратко рассмотрим исследование данных.
Исследование данных
Исследование данных, также известное как исследовательский анализ данных (EDA), – это процесс изучения и визуализации данных, позволяющий с самого начала найти закономерность или раскрыть понимание, а также помогающий выявить проблемы в DataFrame, а также решить, какую модель или алгоритм использовать на последующих этапах.
Давайте кратко обсудим некоторые методы исследования данных
- Статистическая сводка данных
Функция Pyspark describe() используется для просмотра некоторых основных статистических данных, таких как количество, среднее значение, стандартное отклонение и т. д. для DataFrame или ряда числовых…
data = [[1,None,"vignan",95.0], [None,"ojaswi","vvit",55.0], [3,None,"vvit",65.0], [None,"sridevi","vignan",None], [None,"Alrich","nonvignan",56.0], [1,None,None,55.0], [5,"gnanesh","iit",1.0], [1,None,"vignan",95.0], [6,None,"vignan",22.0]] columns = ['student_ID','student_name', 'college','marks'] dataframe = spark.createDataFrame(data,columns) dataframe.describe().show() O/P: +-------+------------------+------------+-------+------------------+ |summary| student_ID|student_name|college| marks| +-------+------------------+------------+-------+------------------+ | count| 6| 4| 8| 8| | mean|2.8333333333333335| null| null| 55.5| | stddev| 2.228601953392904| null| null|32.302144997330615| | min| 1| Alrich| iit| 1.0| | max| 6| sridevi| vvit| 95.0| +-------+------------------+------------+-------+------------------+
- Подсчитать общее количество нулевых значений
Давайте найдем и визуализируем общее количество нулевых значений, присутствующих в столбце DataFrame,
import matplotlib.pyplot as plt null_value_list = list() for col_ in dataframe.columns: null_value_list.append(dataframe.filter(dataframe[col_]. isNull().count()) plt.rcParams["figure.figsize"] = (40,9) columns = [col_ for col_ in dataframe.columns] myexplode = [0.2, 0, 0, 0] plt.pie(null_value_list, labels = columns, explode = myexplode, shadow = True, autopct='%1.0f%%') plt.title('Total number of null value in column') plt.show() O/P:
Как мы видим, столбец «student_ID» имеет 3 нулевых значения, так же как и «student_name» имеет 5 и так далее.
- Подсчитать повторяющиеся строки
Давайте найдем, сколько повторяющихся строк присутствует в DataFrame,
import pyspark.sql.functions as funcs dataframe.groupBy(dataframe.columns).count().where(funcs.col('count') > 1).select(funcs.sum('count')).show() O/P: +----------+ |sum(count)| +----------+ | 2| +----------+
как мы можем показать, в DataFrame есть две повторяющиеся строки, как показано ниже.
- Найти числовой и категориальный столбец
Давайте найдем числовые (!=string) и категориальные (==string) столбцы в DataFrame,
numeric_columns = list() categorical_column = list() for col_ in dataframe.columns: if dataframe.select(col_).dtypes[0][1] != "string": numeric_columns.append(col_) else: categorical_column.append(col_) print("Numeric columns",numeric_columns) print("categorical columns",categorical_column) O/P: Numeric columns ['student_ID', 'marks'] categorical columns ['student_name', 'college']
Теперь у вас есть более четкое представление об исследовании данных. Теперь мы собираемся изучить предварительную обработку данных.
Итак, поехали🚩,
Методы предварительной обработки данных, которые мы собираемся обсудить
- Удалить нулевое значение
- Обработка пропущенного значения с вменением
- Обнаружение выбросов, удаление и вменение
- Удалить функцию / столбец
- Удалить дубликаты
- Преобразовать категориальный признак/столбец в числовой
- Масштабирование функций
- Поезд тестовых разделенных данных
Теперь здесь мы подробно обсудим первые три метода с реализацией. Остальные мы обсудим во второй части этого блога.
Удалить нулевое значение
Давайте сначала создадим Pyspark DataFrame. После создания фрейма данных мы выполним эту операцию.
# Create pyspark dataframe data = [[1,None,"vignan",95.0], [None,"ojaswi","vvit",55.0], [3,None,"vvit",65.0], [None,"sridevi","vignan",None], [None,"Alrich","nonvignan",56.0], [1,None,None,55.0], [5,"gnanesh","iit",1.0], [1,None,"vignan",95.0], [6,None,"vignan",22.0]] # specify column names columns = ['student_ID','student_name', 'college','marks'] # creating a dataframe from the lists of data dataframe = spark.createDataFrame(data,columns) # show dataframe dataframe.show() O/P: +----------+------------+---------+-----+ |student_ID|student_name| college|marks| +----------+------------+---------+-----+ | 1| null| vignan| 95.0| | null| ojaswi| vvit| 55.0| | 3| null| vvit| 65.0| | null| sridevi| vignan| null| | null| Alrich|nonvignan| 56.0| | 1| null| null| 55.0| | 5| gnanesh| iit| 1.0| | 1| null| vignan| 95.0| | 6| null| vignan| 22.0| +----------+------------+---------+-----+
Бум!.. Наш DataFrame создан. Теперь давайте сначала проверим, есть ли какое-либо пропущенное значение в этом фрейме данных.
from pyspark.sql.functions import * print(dataframe.select([count(when(isnan(c) | col(c).isNull(), c)).alias(c) for c in dataframe.columns]).show()) O/P: +----------+------------+-------+-----+ |student_ID|student_name|college|marks| +----------+------------+-------+-----+ | 3| 5| 1| 1| +----------+------------+-------+-----+
Как мы видим, «student_ID», «student_name», «college» и «marks» имеют 3,4,1,1 пропущенных значения соответственно.
Теперь мы отбросим эти недостающие значения. Spark предоставляет функцию dropna(), которая используется для удаления строк с нулевыми значениями в одном или нескольких (любых/всех) столбцах в DataFrame. При чтении данных из файлов Spark API, такие как DataFrame и Dataset, присваивают значения NULL для пустых значений в столбцах.
Что-то, основанное на вашей потребности, нужно удалить эти строки, которые имеют нулевые значения, как часть очистки данных.
dataframe = dataframe.dropna() dataframe.show() O/P: +----------+------------+-------+-----+ |student_ID|student_name|college|marks| +----------+------------+-------+-----+ | 5| gnanesh| iit| 1.0| +----------+------------+-------+-----+
В функции dropna() используются некоторые параметры, вы можете изучить их, просто нажав здесь.
Обрабатывать пропущенное значение с помощью вменения
- Для числового столбца
- Среднее вменение
- Медианное вменение
- Вменение режима
2. Для столбца категорий
- Частое вменение категории
Среднее вменение
Среднее вменение выполняется по числовому признаку/столбцу. В этом импутации отсутствующее значение будет заменено средним определенного признака/столбца.
dataframe.printSchema() O/P: root |-- student_ID: long (nullable = true) |-- student_name: string (nullable = true) |-- college: string (nullable = true) |-- marks: double (nullable = true)
Как мы видим, «student_ID» и «marks» являются числовыми столбцами. Итак, мы выполним среднее вменение в этих столбцах.
from pyspark.ml.feature import Imputer column_subset = [col_ for col_ in dataframe.columns if dataframe.select(col_).dtypes[0][1] !="string"] imputer = Imputer(inputCols=column_subset, outputCols=[col_ for col_ in column_subset] ).setStrategy("mean") dataframe = imputer.fit(dataframe).transform(dataframe) dataframe.show() O/P: +----------+------------+---------+-----+ |student_ID|student_name| college|marks| +----------+------------+---------+-----+ | 1| null| vignan| 95.0| | 2| ojaswi| vvit| 55.0| | 3| null| vvit| 65.0| | 2| sridevi| vignan| 55.5| | 2| Alrich|nonvignan| 56.0| | 1| null| null| 55.0| | 5| gnanesh| iit| 1.0| | 1| null| vignan| 95.0| | 6| null| vignan| 22.0| +----------+------------+---------+-----+
Вменение медианы
Вменение медианы выполняется по числовому признаку/столбцу. В этом импутации отсутствующее значение будет заменено медианой определенного признака/столбца.
from pyspark.ml.feature import Imputer column_subset = [col_ for col_ in dataframe.columns if dataframe.select(col_).dtypes[0][1] !="string"] imputer = Imputer(inputCols=column_subset, outputCols=[col_ for col_ in column_subset] ).setStrategy("median") dataframe = imputer.fit(dataframe).transform(dataframe) dataframe.show() O/P: +----------+------------+---------+-----+ |student_ID|student_name| college|marks| +----------+------------+---------+-----+ | 1| null| vignan| 95.0| | 2| ojaswi| vvit| 55.0| | 3| null| vvit| 65.0| | 2| sridevi| vignan| 55.5| | 2| Alrich|nonvignan| 56.0| | 1| null| null| 55.0| | 5| gnanesh| iit| 1.0| | 1| null| vignan| 95.0| | 6| null| vignan| 22.0| +----------+------------+---------+-----+
Вменение режима
Вменение режима выполняется по числовому признаку/столбцу. В этом импутации отсутствующее значение будет заменено режимом конкретного признака/столбца.
column_subset = [col_ for col_ in dataframe.columns if dataframe.select(col_).dtypes[0][1] !="string"] for col_ in column_subset: temp_col = dataframe.groupBy(col_).count() temp_col = temp_col.dropna(subset=col_) mode = temp_col.orderBy(temp_col['count'].desc()).collect()[0] [0] dataframe = dataframe.fillna(mode, subset=col_) dataframe.show() O/P: +----------+------------+---------+-----+ |student_ID|student_name| college|marks| +----------+------------+---------+-----+ | 1| null| vignan| 95.0| | 1| ojaswi| vvit| 55.0| | 3| null| vvit| 65.0| | 1| sridevi| vignan| 95.0| | 1| Alrich|nonvignan| 56.0| | 1| null| null| 55.0| | 5| gnanesh| iit| 1.0| | 1| null| vignan| 95.0| | 6| null| vignan| 22.0| +----------+------------+---------+-----+
Частое вменение категорий
Теперь мы увидели весь метод вменения для числового столбца. Что, если столбец DataFrame является категориальным?
Для этого мы будем использовать метод частого вменения категорий. В этом методе мы вменяем пропущенное значение по часто встречающейся категории.
column_subset = [col_ for col_ in dataframe.columns if dataframe.select(col_).dtypes[0][1] !="string"] for col_ in column_subset: temp_col = dataframe.groupBy(col_).count() temp_col = temp_col.dropna(subset=col_) frequent_category=temp_col.orderBy( temp_col['count'].desc()).collect()[0][0] dataframe = dataframe.fillna(frequent_category, subset=col_) dataframe.show() O/P: +----------+------------+---------+-----+ |student_ID|student_name| college|marks| +----------+------------+---------+-----+ | 1| ojaswi| vignan| 95.0| | null| ojaswi| vvit| 55.0| | 3| ojaswi| vvit| 65.0| | null| sridevi| vignan| null| | null| Alrich|nonvignan| 56.0| | 1| ojaswi| vignan| 55.0| | 5| gnanesh| iit| 1.0| | 1| ojaswi| vignan| 95.0| | 6| ojaswi| vignan| 22.0| +----------+------------+---------+-----+
Давайте разберем приведенный выше код для лучшего понимания.
column_subset = [col_ for col_ in dataframe.columns if dataframe.select(col_).dtypes[0][1] !="string"] for col_ in column_subset: temp_col = dataframe.groupBy(col_).count() temp_col = temp_col.dropna(subset=col_) frequent_category = temp_col.orderBy( temp_col['count'].desc()).show() O/P: +------------+-----+ |student_name|count| +------------+-----+ | ojaswi| 6| | Alrich| 1| | sridevi| 1| | gnanesh| 1| +------------+-----+ +---------+-----+ | college|count| +---------+-----+ | vignan| 5| | iit| 1| | vvit| 2| |nonvignan| 1| +---------+-----+
Как мы могли видеть, приведенный выше код дал нам общее количество произошедших категорий времени. Итак, теперь мы преобразуем frequent_category в RDD, чтобы собирать наиболее часто встречающиеся элементы, как показано ниже.
frequent_category = temp_col.orderBy( temp_col['count'].desc()).collect()[0][0] print(frequent_category) O/P: ojaswi vignan
И бум!! И здесь мы получили часто встречающиеся элементы. Теперь просто впишите их вместо пропущенного значения.
Обнаружение, удаление и импутация выбросов
Что такое выброс?
С точки зрения непрофессионала выбросы — это точки данных, которые значительно отличаются от наблюдений. Или мы можем сказать, что выбросы — это «одна из этих вещей не похожа на другие».😉
Но подождите!✋🤚
Действительно ли выбросы опасны?🤔
Выбросы не всегда опасны для нашей постановки задачи. На самом деле выбросы иногда могут быть полезными индикаторами.
Например, обнаружение мошенничества с кредитными/дебетовыми картами. Индустрия кредитных/дебетовых карт использовала концепцию выбросов при обнаружении мошенничества с кредитными картами в прошлом и в настоящее время.
Хорошо, теперь вы получили представление о выбросах, поэтому ваш вопрос в том, как обнаружить выбросы, верно? Давайте углубимся в это.
Обнаружение выбросов
Существует много методов обнаружения выбросов, но здесь мы в основном сосредоточимся на следующих двух методах.
- Использование Z-оценки
- Использование ИКР
Использование Z-оценки
Z-оценка — это параметрический метод обнаружения выбросов в одномерном или маломерном пространстве признаков.
Этот метод предполагает нормальное распределение/гауссово распределение данных. Выбросы — это точки данных, которые находятся в хвостах распределения и, следовательно, далеки от среднего значения.
Нормальное распределение/распределение Гаусса показано выше, и предполагается, что
68% точек данных лежат в пределах +/- 1 стандартного отклонения.
95% точек данных лежат между +/- 2 стандартным отклонением
99,7% точек данных лежат между +/- 3стандартное отклонение
Мы можем найти Z-оценку, используя приведенную ниже формулу
Z-оценка = (x -среднее) / стандартное отклонение
Если z-оценка точки данных больше +/- 3, это означает, что точка данных сильно отличается от других точек данных. Такая точка данных может быть выбросом.
Найдем выброс,
Чтобы найти выброс, сначала давайте создадим Pyspark DataFrame с выбросом.
data = [["Patty O’Furniture",5.9], ["Paddy O’Furniture",5.2], ["Olive Yew",5.1], ["Aida Bugg",5.5], ["Maureen Biologist",4.9], ["Teri Dacty",5.4], ["Peg Legge",6.2], ["Allie Grate",6.5], ["Liz Erd",7.1], ["A. Mused",14.5], ["Constance Noring",6.1], ["Lois Di Nominator",5.6], ["Minnie Van Ryder",1.2], ["Lynn O’Leeum",5.5]] columns = ['student_name','height'] dataframe = spark.createDataFrame(data,columns) dataframe.show() O/P: +-----------------+------+ | student_name|height| +-----------------+------+ |Patty O’Furniture| 5.9| |Paddy O’Furniture| 5.2| | Olive Yew| 5.1| | Aida Bugg| 5.5| |Maureen Biologist| 4.9| | Teri Dacty| 5.4| | Peg Legge| 6.2| | Allie Grate| 6.5| | Liz Erd| 7.1| | A. Mused| 14.5| | Constance Noring| 6.1| |Lois Di Nominator| 5.6| | Minnie Van Ryder| 1.2| | Lynn O’Leeum| 5.5| +-----------------+------+
Теперь давайте обнаружим выброс в приведенном выше DataFrame, используя метод Z-оценки.
from pyspark.sql.functions import * column_subset = dataframe.columns for col in column_subset: if dataframe.select(col).dtypes[0][1]=="string": pass else: mean = dataframe.select(mean(col)).collect()[0][0] stddev = dataframe.select(stddev(col)).collect()[0][0] upper_limit = mean + (3*stddev) lower_limit = mean - (3*stddev) dataframe = dataframe.filter((dataframe[col]<lower_limit) | (dataframe[col]>upper_limit)) dataframe.show() O/P: +------------+------+ |student_name|height| +------------+------+ | A. Mused| 14.5| +------------+------+ Note: Outlier can be found in numerical column only
Как мы видим, student_name=A. Musedимеет 14,5 высоту, что является исключением.
И бюст!! ⚡ у вас есть четкое представление о том, как найти выброс. Теперь давайте углубимся в технику удаления выбросов и вменения выбросов.
Удаление выброса Z-показателя
from pyspark.sql.functions import * column_subset = dataframe.columns for col in column_subset: if dataframe.select(col).dtypes[0][1]=="string": pass else: mean = dataframe.select(mean(col)).collect()[0][0] stddev = dataframe.select(stddev(col)).collect()[0][0] upper_limit = mean + (3*stddev) lower_limit = mean - (3*stddev) dataframe = dataframe.filter((dataframe[col]>lower_limit) & (dataframe[col]<upper_limit)) dataframe.show() O/P: +-----------------+------+ | student_name|height| +-----------------+------+ |Patty O’Furniture| 5.9| |Paddy O’Furniture| 5.2| | Olive Yew| 5.1| | Aida Bugg| 5.5| |Maureen Biologist| 4.9| | Teri Dacty| 5.4| | Peg Legge| 6.2| | Allie Grate| 6.5| | Liz Erd| 7.1| | Constance Noring| 6.1| |Lois Di Nominator| 5.6| | Minnie Van Ryder| 1.2| | Lynn O’Leeum| 5.5| +-----------------+------+
Как мы видим, student_name=A. У Musedвысота 14,5 (что является выбросом) была удалена.
Вменение выброса Z-показателя
Мы можем вычислить выброс по среднему или медиане конкретного признака/столбца.
Здесь мы увидим среднее вменение.
from pyspark.sql import Window from pyspark.sql.functions import * column_subset = dataframe.columns for col in column_subset: if dataframe.select(col).dtypes[0][1]=="string": pass else: mean = dataframe.select(mean(col)).collect()[0][0] stddev = dataframe.select(stddev(col)).collect()[0][0] upper_limit = mean + (3*stddev) lower_limit = mean - (3*stddev) dataframe = dataframe.withColumn(col,when((dataframe[col] <lower_limit) | (dataframe[col]>upper_limit), round(mean(col).over(Window.orderBy(lit(1)))).cast('int')).otherwise(dataframe[col])) dataframe.show() O/P: +-----------------+------+ | student_name|height| +-----------------+------+ |Patty O’Furniture| 5.9| |Paddy O’Furniture| 5.2| | Olive Yew| 5.1| | Aida Bugg| 5.5| |Maureen Biologist| 4.9| | Teri Dacty| 5.4| | Peg Legge| 6.2| | Allie Grate| 6.5| | Liz Erd| 7.1| | A. Mused| 6.0| | Constance Noring| 6.1| |Lois Di Nominator| 5.6| | Minnie Van Ryder| 1.2| | Lynn O’Leeum| 5.5| +-----------------+------+
Посмотрите, мы успешно вменили выброс по среднему значению этого признака/столбца. “| А. Мьюд | 6.0|”.
Таким же образом вы можете вычислить выброс по медиане.
Это все о методе обнаружения выбросов Z-оценки. Теперь давайте рассмотрим метод обнаружения выбросов IQR.
Использование IQR
IQR (межквартильный размах) – это мера статистической дисперсии, то есть разброса данных. IQR также может называться средним спредом, средним 50% или H-спредом. Он определяется как разница между 75-м и 25-м процентили данных.
or
IQR (межквартильный диапазон) определяет разницу между третьим и первым квартилем. Квартили — это разделенные значения, которые делят весь ряд на 4 равные части. Итак, есть 3 квартили. Первый квартиль обозначается Q1, известный как нижний квартиль, второй квартиль обозначается Q2. а третий квартильобозначается Q3, известный как верхний квартиль. Следовательно, межквартильный диапазон равен верхней квартилю минус нижний квартиль.
Итак, формула IQR:
IQR = Q3 - Q1
Чтобы обнаружить выбросы с помощью этого метода, мы определяем новый диапазон, назовем его диапазоном решений, и любая точка данных, лежащая за пределами этого диапазона, считается выбросом и соответствующим образом обрабатывается. Диапазон указан ниже:
Lower Bound: (Q1 - 1.5 * IQR) Upper Bound: (Q3 + 1.5 * IQR)
Любая точка данных меньше нижней границы илибольше верхней границы считается выбросом.
Давайте найдем выброс в приведенном выше DataFrame.
for col_ in dataframe.columns: if dataframe.select(col_).dtypes[0][1]=="string": pass else: q1,q3 = dataframe.approxQuantile(col_,[0.25, 0.75],0) IQR = q3 - q1 lower_bound = q1 - (1.5*IQR) upper_bound = q3 + (1.5*IQR) dataframe = dataframe.filter((dataframe[col]<lower_bound) | (dataframe[col]>upper_bound)) dataframe.show() O/P: +----------------+------+ | student_name|height| +----------------+------+ | A. Mused| 14.5| |Minnie Van Ryder| 1.2| +----------------+------+ Note: Outlier can be found in numerical column only
Как мы видим, А. MusedиМинни Ван Райдерявляютсявыдающимися.
Теперь давайте углубимся в технику удаления выбросов IQR и метода импутации выбросов.
Удаление выбросов IQR
for col_ in dataframe.columns: if dataframe.select(col_).dtypes[0][1]=="string": pass else: q1,q3 = dataframe.approxQuantile(col_,[0.25, 0.75],0) IQR = q3 - q1 lower_bound = q1 - (1.5*IQR) upper_bound = q3 + (1.5*IQR) dataframe = dataframe.filter((dataframe[col]>lower_bound) & (dataframe[col]<upper_bound)) dataframe.show() O/P: +-----------------+------+ | student_name|height| +-----------------+------+ |Patty O’Furniture| 5.9| |Paddy O’Furniture| 5.2| | Olive Yew| 5.1| | Aida Bugg| 5.5| |Maureen Biologist| 4.9| | Teri Dacty| 5.4| | Peg Legge| 6.2| | Allie Grate| 6.5| | Liz Erd| 7.1| | Constance Noring| 6.1| |Lois Di Nominator| 5.6| | Lynn O’Leeum| 5.5| +-----------------+------+
Как мы видим, student_name=A. Mused и Minnie Van Ryder (выпадающие из списка) были удалены.
Вменение выбросов IQR
Мы можем вычислить выброс по среднему или медиане конкретного признака/столбца.
Здесь мы увидим среднее вменение.
for col_ in dataframe.columns: if dataframe.select(col_).dtypes[0][1]=="string": pass else: q1,q3 = dataframe.approxQuantile(col_,[0.25, 0.75],0) IQR = q3 - q1 lower_bound = q1 - (1.5*IQR) upper_bound = q3 + (1.5*IQR) dataframe = dataframe.withColumn(col_,when((dataframe[col_]<lower_bound) (dataframe[col_]>upper_bound),round(mean(col_).over(Window.orderBy(lit(1))))).otherwise(dataframe[col_])) dataframe.show() O/P: +-----------------+------+ | student_name|height| +-----------------+------+ |Patty O’Furniture| 5.9| |Paddy O’Furniture| 5.2| | Olive Yew| 5.1| | Aida Bugg| 5.5| |Maureen Biologist| 4.9| | Teri Dacty| 5.4| | Peg Legge| 6.2| | Allie Grate| 6.5| | Liz Erd| 7.1| | A. Mused| 6.0| | Constance Noring| 6.1| |Lois Di Nominator| 5.6| | Minnie Van Ryder| 6.0| | Lynn O’Leeum| 5.5| +-----------------+------+
Посмотрите, мы успешно вменили выброс по среднему значению этого признака/столбца. “| А. Мьюд | 6,0|” и “| Минни Ван Райдер| 6.0|».
Заключение
Итак, подводя итог, мы поняли обзор Pyspark, удалить нулевое значение, обработать отсутствующее значение с вменением, обнаружение выбросов, удаление и вменение. Мы видели эти методы в реализации в Pyspark и Python.
Теперь, во второй части, мы обсудим удаление признаков/столбцов, удаление дубликатов, преобразование категориальных признаков/столбцов в числовые, масштабирование признаков, обучение разделенных данных тестирования, поэтому следите за обновлениями😊 и будьте здоровы(●' ◡'●).
Спасибо за чтение!
Есть ли у вас вопросы? если да, свяжитесь со мной в LinkedIn — Vishal Barad. Рад общению!
Мы будем очень признательны за любой отзыв, и если вам понравилось то, что вы прочитали, нажмите и удерживайте кнопку хлопка!