Отток клиентов, когда клиенты уходят из компании, может существенно повлиять на итоговую прибыль компании. Отток клиентов — это процесс, когда клиенты прекращают свои отношения с бизнесом, будь то отмена подписки, закрытие учетной записи или прекращение покупок.

Отток может происходить по разным причинам, таким как неудовлетворенность продуктом или услугой, предложениями конкурентов или изменением личных обстоятельств. Потеря клиентов приводит не только к прямой потере дохода, но и к дополнительным затратам, связанным с приобретением новых клиентов взамен потерянных. Более того, отток клиентов может запятнать репутацию компании и ослабить имидж ее бренда.

Введение модели XGBoost

Чтобы эффективно бороться с оттоком клиентов, предприятия могут использовать передовые методы аналитики и машинного обучения. Одним из таких мощных инструментов является модель XG Boost. XG Boost (сокращение от Extreme Gradient Boosting) – это популярный алгоритм машинного обучения, используемый для задач контролируемого обучения, особенно в области прогнозного моделирования и классификации. Он принадлежит к семейству алгоритмов повышения градиента, которые объединяют прогнозы нескольких слабых моделей (обычно деревьев решений) для создания надежной прогнозной модели.

XGBoost включает в себя несколько улучшений по сравнению с традиционными алгоритмами повышения градиента, что делает его очень эффективным и действенным. Некоторые примечательные функции XGBoost включают в себя.

XGBoost завоевал популярность в различных областях, включая финансы, здравоохранение, рекламу и многое другое. Благодаря своей исключительной производительности и универсальности модель XG Boost можно использовать для анализа данных об оттоке клиентов и получения полезных сведений.

Типичные задачи построения модели машинного обучения

Создание модели машинного обучения включает в себя различные задачи, каждая из которых имеет решающее значение для обеспечения качества и надежности конечной модели.

Эти задачи типичны для проекта машинного обучения и могут способствовать успеху проекта при тщательном выполнении. Но они могут потребовать много опыта и времени, и у каждого есть свои проблемы.

Хотя мы можем не охватывать все задачи, упомянутые в списке, каждый шаг способствует пониманию данных и раскрытию важных идей.

Исследовательский анализ данных (EDA)

Исследовательский анализ данных (EDA) является важным шагом в процессе построения модели. Выполняя EDA, вы получите более глубокое понимание данных, определите потенциальные проблемы или ограничения и проинформируете о последующих шагах конвейера анализа данных, таких как выбор модели, разработка функций и прогнозное моделирование. EDA помогает выявлять закономерности, взаимосвязи и аномалии, обеспечивая принятие решений на основе данных и полезную информацию.

В этом руководстве мы пройдем несколько важных этапов процесса исследовательского анализа данных (EDA). К концу этого руководства вы будете вооружены знаниями, необходимыми для эффективного выполнения методов исследовательского анализа данных (EDA). Вы получите представление об очистке данных, визуализации и статистическом анализе, что позволит вам выявить закономерности, тенденции и выбросы в ваших наборах данных.

Шаг 1: Подготовка данных

Подготовка ваших данных для модели машинного обучения является важным шагом в этом процессе. Этот этап, также известный как предварительная обработка данных или очистка данных, может сделать ваш проект успешным или разрушить его.

Jupyter Notebook — платформа для написания кода Python

Для приведенного ниже пошагового руководства я использовал Jupyter Notebook для построения модели оттока клиентов. Набор данных клиентов телекоммуникационных компаний был выбран в качестве практической иллюстрации в этом случае.

Набор данных общедоступен, его можно найти здесь: https://github.com/IBM/telco-customer-churn-on-icp4d.

Первый шаг включает в себя настройку среды Jupyter Notebook, в которой мы можем писать и выполнять код Python.

Почему стоит выбрать Jupyter Notebook? Я упоминал в своей предыдущей статье: Прогнозирование временных рядов — 6 шагов для построения LSTM-модели прогнозирования цен на акции.

Вот общие ярлыки для Jupyter Notebook для справки:

Установите и импортируйте необходимые библиотеки pandas

Используя оператор import pandas as pd, вы делаете библиотеку pandas доступной в своем сценарии или записной книжке Python, что позволяет вам использовать ее широкие возможности для обработки, анализа и исследования данных.

1. Загрузите набор данных и изучите данные

Код считывает файл CSV, расположенный по указанному пути к файлу, с помощью pd.read_csv(), сохраняет данные во фрейме данных pandas с именем data_df, а затем отображает первые несколько строк фрейма данных с помощью data_df.head().

Набор данных состоит из 21 столбца, включая числовые и категориальные переменные. Столбцы:

customerID: уникальный идентификатор, который идентифицирует каждого клиента.

пол: пол клиента.

SeniorCitizen: является ли клиент пожилым гражданином или нет (1, 0).

Партнер: есть ли у клиента партнер или нет (да, нет).

Иждивенцы: есть ли у клиента иждивенцы или нет (да, нет).

длительность: количество месяцев, в течение которых клиент оставался в компании.

PhoneService: есть ли у клиента телефонная служба или нет (да, нет).

Несколько линий: есть ли у клиента несколько линий или нет (да, нет, нет телефонной службы).

InternetService: интернет-провайдер клиента (DSL, оптоволокно, нет).

Онлайн-безопасность: есть ли у клиента онлайн-защита или нет (да, нет, нет интернет-службы).

OnlineBackup: есть ли у клиента онлайн-резервное копирование или нет (да, нет, нет интернет-службы).

DeviceProtection: есть ли у клиента защита устройства или нет (да, нет, нет интернет-сервиса).

Техническая поддержка: есть ли у клиента техническая поддержка или нет (да, нет, нет интернет-службы).

Потоковое ТВ: есть ли у клиента потоковое телевидение или нет (да, нет, нет интернет-сервиса).

StreamingMovies: есть ли у клиента потоковое воспроизведение фильмов или нет (да, нет, нет интернет-сервиса).

Контракт: срок действия контракта с клиентом (ежемесячно, один год, два года).

Безбумажный биллинг: есть ли у клиента безбумажный биллинг или нет (да, нет).

Способ оплаты: способ оплаты клиента (электронный чек, чек по почте, банковский перевод (автоматически), кредитная карта (автоматически)).

Ежемесячные платежи: сумма, взимаемая с клиента ежемесячно.

TotalCharges: общая сумма, списанная с клиента.

Отток. Ушел ли клиент или нет (да, нет).

Целевой переменной является Churn, которую мы хотим предсказать.

Получение формы набора данных

Это показывает 7043 строки и 21 столбец/функция.

Отображение первых 5 строк набора данных

Игра с данными

Результатом кода является отображение первых двух значений в столбце «пол» кадра данных data_df. На основе предоставленного вывода: 0 женщин; 1 мужчина.

Первая строка имеет значение «Женский» в столбце «Пол», а вторая строка имеет значение «Мужской». «dtype: object» указывает, что тип данных столбца «gender» — «object», что является распространенным представлением текстовых или категориальных данных в pandas.

Код фильтрует кадр данных data_df на основе условия и извлекает первые две строки, удовлетворяющие этому условию. Сегмент data_df[data_df.gender == ‘Female’] фильтрует кадр данных data_df на основе условия data_df.gender == ‘Female’. Он выбирает строки, в которых значение в столбце «пол» равно «женский».

Получить представление о типе столбца и наличии нулевых значений

Код загрузил набор данных в DataFrame, и отображаемый результат предоставляет обзор структуры DataFrame, включая имена столбцов, ненулевые счетчики и типы данных каждого столбца. Результат помогает понять состав и характеристики набора данных перед дальнейшим анализом или обработкой.

Dtype — это либо int/float, то есть числовые значения, либо объект, что означает, что панды считают их категориальными значениями. Столбец/функция tenure имеет нулевое значение.

В случае, если TotalCharges обнаружен как объект, используйте строку ниже, чтобы преобразовать его в числовое значение. Параметр coerce будет вводить NaN, если невозможно преобразовать значение в числовое.

data_df.TotalCharges = pd.to_numeric(data_df.TotalCharges, errors=’coerce’)

2. Обработка пропущенных значений

Похоже, что в наборе данных отсутствуют некоторые значения в столбцах tenure и TotalCharges, так как их количество меньше, чем общее количество строк (7043). Давайте проверим пропущенные значения в наборе данных.

Например, AWS Datawig использует нейронные сети для прогнозирования пропущенных значений табличных данных https://github.com/awslabs/datawig.

isna() возвращает True/False для каждой строки, если значение NaN. Ниже приведен фильтр данных. В столбце срока пребывания отсутствует 51 значение.

Для столбца срока пребывания, поскольку это числовой столбец, мы можем заполнить недостающие значения средним сроком владения.

Теперь смотрим на TotalCharges.

Столбец TotalCharges содержит 11 отсутствующих значений, а также является числовым столбцом и представляет общую сумму, выставленную клиенту. Отсутствие значения в этом столбце может означать, что с клиента еще не была снята оплата. Давайте удалим эти 11 строк и проверим номер строки, чтобы подтвердить удаление.

3. Несбалансированный набор данных

Дисбаланс классов — удивительно распространенная проблема в машинном обучении (особенно в классификации), возникающая в наборах данных с непропорциональным соотношением наблюдений в каждом классе.

То есть количество выборок на класс распределяется неравномерно.

Модель ML может не работать для нечастых классов, так как недостаточно образцов для изучения шаблонов, классификатору будет сложно идентифицировать/сопоставить эти шаблоны.

Помимо изменения показателей производительности с точности на точность, полноту и/или оценку f1, другие способы устранения дисбаланса классов в основном связаны с балансировкой либо набора данных, либо способа обучения алгоритма ML на наборе данных.

XGBoost имеет аналогичный механизм (гиперпараметр scale_pos_weight) для обработки несбалансированных наборов данных.

Шаг 2: Профилирование данных

Для числовых столбцов description() дает вам среднее значение, стандартное отклонение, минимум, максимум и т. д.

Сводная статистика дает некоторые полезные сведения:

Столбец Tenage имеет минимальное значение 0 и максимальное значение 72. В среднем клиенты остаются в компании около 32 месяцев.

В столбце SeniorCitizen указано, что около 16 % клиентов — пожилые люди.

Столбец MonthlyCharges имеет минимальное значение 18,25 и максимальное значение 118,75. В среднем с клиентов взимается около 64,76 в месяц.

Столбец TotalCharges имеет минимальное значение 18,8 и максимальное значение 22000. В среднем общая сумма, списанная с клиентов, составляет около 2291,46.

Получите разные значения для конкретной категориальной функции/столбца

Примечание. data_df.gender эквивалентно data_df[‘gender’].

Получите мощность этих категориальных функций

Примечание: это будет полезно позже при кодировании этих функций в числовые представления.

Вы можете видеть, что некоторые из этих функций имеют значения «да/нет» (кардинальность = 2), поэтому мы превратим их в значения 0/1.

Код применяет преобразования к выбранным столбцам в DataFrame data_df. Он преобразует категориальные значения («нет» или «мужской») в двоичные числовые значения (0 или 1), что делает их пригодными для последующего анализа или задач моделирования. Преобразования выполняются с помощью функции apply и лямбда-функций, а результирующие столбцы преобразуются в числовой тип данных с помощью pd.to_numeric.

  1. list_of_cols_to_transform = [‘Партнер’, ‘Зависимые’, ‘PhoneService’, ‘Безбумажный биллинг’, ‘Отток’]. Эта строка определяет список с именем list_of_cols_to_transform, содержащий имена столбцов, которые необходимо преобразовать.
  2. для столбца в list_of_cols_to_transform:эта строка запускает цикл, который перебирает имя каждого столбца в list_of_cols_to_transform.
  3. data_df[col] = data_df[col].apply(lambda x: 0 if str(x).lower() == ‘no’ else 1):Эта строка использует функцию apply для применения лямбда-функции к каждому элементу указанного столбца. Лямбда-функция преобразует значение в 0, если оно равно «нет» (без учета регистра), и преобразует его в 1 в противном случае. Результат присваивается тому же столбцу в DataFrame.
  4. data_df[col] = pd.to_numeric(data_df[col]):Эта строка преобразует значения в указанном столбце в числовой тип данных. Это гарантирует, что столбец содержит числовые значения, которые можно использовать для дальнейших вычислений или анализа.
  5. data_df[‘gender’] = data_df[‘gender’].apply(lambda x: 0 if str(x).lower() == ‘male’ else 1):Эта строка выполняет аналогичное преобразование специально для столбца ‘gender’. Он преобразует значение в 0, если оно равно «мужскому» (без учета регистра), и преобразует его в 1 в противном случае.
  6. data_df[‘gender’] = pd.to_numeric(data_df[‘gender’]):Эта строка преобразует столбец «пол» в числовой тип данных, аналогично тому, что было сделано для других столбцов.

Шаг 3: Корреляционный анализ

Корреляции играют решающую роль в исследовательском анализе данных (EDA), поскольку они помогают выявить взаимосвязи между переменными в наборе данных. В EDA корреляции дают представление о том, как связаны переменные, и могут помочь понять закономерности, лежащие в основе данных.

Сильные положительные или отрицательные корреляции между функциями могут негативно повлиять на производительность некоторых моделей машинного обучения, таких как модели линейной регрессии и логистической регрессии.

  • Чтобы решить эту проблему, один из подходов состоит в том, чтобы выбрать одну из коррелирующих функций и удалить другие, чтобы смягчить влияние мультиколлинеарности.
  • Однако стоит отметить, что другие модели машинного обучения, такие как деревья решений, как правило, менее подвержены этой проблеме и могут более эффективно обрабатывать сильно коррелированные функции.

С другой стороны, когда функции сильно коррелируют с целевой переменной, модели линейной и логистической регрессии могут выиграть от повышения производительности.

Корреляционная матрица

Давайте создадим матрицу корреляции для нашего набора данных.

для создания корреляционной матрицы нам нужны числовые значения, поэтому нам нужно преобразовать наши категориальные признаки в числовые. Для этого мы генерируем одно представление горячего кодирования этих категориальных функций.

Краткое объяснение кодирования категориальных признаков

мы будем использовать это для корреляционной матрицы, но особенно непосредственно перед обучением нашей модели, как только мы закончим разработку функций.

Кодирование категориальных признаков

Кодирование категориальных признаков – это процесс преобразования категориальных переменных в числовые представления, которые могут использоваться алгоритмами машинного обучения. Вот несколько примеров методов кодирования категориальных признаков:

Горячее кодирование. Пример. Рассмотрим категориальный признак «Цвет» со значениями [‘Красный’, ‘Синий’, ‘Зеленый’]. После горячего кодирования он будет представлен в виде трех бинарных признаков: «Цвет_Красный», «Цвет_Синий», «Цвет_Зеленый». Каждая функция будет иметь значение 1 или 0, чтобы указать наличие или отсутствие соответствующей категории.

Порядковое кодирование. Пример. Рассмотрим категориальный признак «Рейтинг» со значениями [«Плохо», «Удовлетворительно», «Хорошо», «Отлично»]. Порядковое кодирование присваивает числовое значение на основе порядка или ранжирования категорий. В этом случае его можно было бы закодировать как [0, 1, 2, 3], отражая возрастающий порядок оценок.

Это всего лишь несколько примеров методов кодирования. Выбор метода кодирования зависит от характера категориальных признаков, количества категорий и конкретных требований используемого алгоритма машинного обучения.

Пример:

Функция encode_categorical_features принимает в качестве входных данных кадр данных, идентифицирует столбцы категорий, выполняет однократное кодирование этих столбцов и возвращает результирующий кадр данных с заменой категорийных признаков закодированными признаками.

Сгенерируйте корреляционную матрицу

Подробнее о корреляционных матрицах: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.corr.html

Как рассчитывается коэффициент корреляции Пирсона? https://en.wikipedia.org/wiki/Коэффициент_корреляции_Пирсона

Определите 5 лучших пар характеристик и метокс самой высокой корреляцией (=показатель Пирсона), положительной или отрицательной (абсолютное значение).

Это говорит нам о том, что эти 5 функций имеют самую высокую линейную зависимость (прямую связь) с целью среди всех функций.

Шаг 4: Визуализация данных

Matplotlib — это библиотека goto для построения графиков в блокнотах Jupyter. Seaborn — это оболочка над matplotlib, упрощающая ее использование. Метод plot для Series и DataFrame — это просто оболочка для plt.plot. В своей предыдущей статье я упомянул другую библиотеку визуализации под названием Plotly. Если вам интересно, я рекомендую вам изучить Plotly для ваших нужд визуализации.

Одномерная статистика — простая гистограмма для просмотра одного столбца/функции.

Глядя на распределение владения

Гистограммы показывают распределение числовых данных. Данные разделены на «сегменты» или «бины».

Глядя на состав определенных функций

Диаграммы FacetGrid с Seaborn

Диаграммы FacetGrid в Seaborn предлагают несколько преимуществ. Они особенно полезны для изучения вариаций комбинаций категориальных признаков и позволяют эффективно сравнивать и выявлять различия в подмножествах данных.

В упомянутом конкретном примере диаграмма FacetGrid показывает, что ежемесячные контракты демонстрируют более высокий коэффициент оттока, особенно в случаях, когда нет интернет-сервиса. Это понимание подчеркивает важность типа контракта и доступности Интернета для влияния на отток клиентов.

Глядя на отношения между 2 переменными

График рассеяния

Показывает, насколько сильно связаны пары признаков. В этом примере мы показываем очевидную корреляцию между общими расходами и ежемесячными платежами, а также несколько отклонений.

Создайте тепловую карту, чтобы визуализировать корреляцию пар

  • Сиборн

  • сюжетно

Вот некоторые выводы из визуализаций:

Пол. Коэффициент оттока практически одинаков для обоих полов. Таким образом, пол, похоже, не оказывает существенного влияния на отток.

Пожилой гражданин. Пожилые люди имеют более высокий коэффициент оттока по сравнению с обычными людьми.

Партнер. Клиенты, не имеющие партнера, как правило, уходят чаще, чем те, у кого есть партнер.

Иждивенцы. Клиенты без иждивенцев, как правило, уходят больше, чем те, у кого есть иждивенцы.

Телефонная связь. Уровень оттока не сильно отличается между клиентами с телефонной связью и без нее.

Интернет-услуги. Клиенты с оптоволоконным доступом в Интернет, как правило, теряют больше всего. Те, у кого нет интернета, меньше всего теряют.

Контракт. У клиентов с ежемесячным контрактом процент оттока значительно выше, чем у клиентов с годовым или двухлетним контрактом.

Способ оплаты. Клиенты, которые платят электронным чеком, имеют более высокий уровень оттока.

Эти идеи могут помочь нам понять поведение клиентов и факторы, которые могут повлиять на их решение об оттоке.

Я скомпилировал весь код в фрагменты кода для вашего ознакомления.

# Step 1: Data preparation
  #### Install and import necessary pandas libraries
    !pip install pandas
    import pandas as pd

 ## 1. Load dataset and understand data
    address='C:....bank-additional-full.csv'
    Customer_Pre =  pd.read_csv(address, delimiter=';')
    Customer_Pre.head()

  #### Getting the shape of the dataset
    data_df.shape

  #### Display the first 5 line of the dataset
    data_df.head(5)

  #### Playing with the data
    data_df['gender'].head(2)
    
    data_df[data_df.gender == 'Female'].head(2)
  
  #### Get a view of the column type and presence of null values
    data_df.info()


 ## 2.handle Missing Values
    data_df.info()
    
    data_df['tenure'] = data_df['tenure'].fillna(data_df['tenure'].mean())
    
    data_df[data_df['TotalCharges'].isna()].head(20)
    
    data_df = data_df[data_df['TotalCharges'].notna()]
    
    data_df.shape
  
  
 ## 3. Imbalanced Dataset
    sns.countplot(x='Churn', data=data_df)
    plt.show()

 ############################################################################

# Step 2: Data profiling 

  #### For numeric columns, describe() gives you mean, standard deviation, min, max etc
    data_df.describe()
  
  #### Get the different values for a specific categorical feature/column
    data_df.gender.value_counts()
  
  #### Get the cardinality of those categorical features
    cardinality_pd_series = data_df.apply(pd.Series.nunique)
    cardinality_pd_series
    
    list_of_cols_to_transform = ['Partner', 'Dependents', 'PhoneService', 'PaperlessBilling', 'Churn']
    
    for col in list_of_cols_to_transform:
        data_df[col] = data_df[col].apply(lambda x: 0 if str(x).lower() == 'no' else 1)
        data_df[col] = pd.to_numeric(data_df[col])
    
    data_df['gender'] = data_df['gender'].apply(lambda x: 0 if str(x).lower() == 'male' else 1)
    data_df['gender'] = pd.to_numeric(data_df['gender'])

##############################################################################

# Step 3: Correlation analysis

  #### Encoding Categorical Features
      def encode_categorical_features(df):
      # getting a list of categorical columns
      categorical_col = []
      for col in df.columns:
          col_dtype = df[col].dtype
          if col_dtype == 'object':
              categorical_col.append(col)
      # generating the one hot encoded features        
      df = pd.get_dummies(df, columns=categorical_col)
                             
      print(df.shape)
      return df
  
      # dropping the customerID as it is of no use for the correlation matrix
      corr_data_df = encode_categorical_features(data_df.drop('customerID', axis=1))
      
      corr_data_df.head(5)

  #### Generate the correlation matrix
    corrmat = corr_data_df.corr()
    
    corrmat.head(5)
    
    corrmat['Churn'].apply(lambda x: abs(x)).sort_values(ascending=False).drop('Churn').head(5)

##############################################################################

# Step 4: Data Visualisation
    import matplotlib.pyplot as plt
    %matplotlib inline
    import seaborn as sns
    
    data_df.gender.value_counts().plot.bar()
    plt.show()
    
    sns.countplot(x='gender', data=data_df)
    plt.show()
  
  #### Looking at the distribution of the tenure
    sns.distplot(data_df['tenure'], bins=50, kde=False)
    plt.show()
  
  #### Looking at the composition of certain features
    data_df['InternetService'].value_counts().plot.pie(y='InternetService')
    plt.show()
  
  #### FacetGrid charts with seaborn
    grid = sns.FacetGrid(data_df, col='Contract', row='InternetService', height=2.2, aspect=2)
    grid.map(sns.countplot, 'Churn')
    grid.add_legend();
    plt.show()
  
  #### Looking at the relationship between 2 variables
  #### Scatter plot
    sns.scatterplot(data=data_df.sample(frac=1), x='TotalCharges', y='MonthlyCharges')
    plt.show()
  
   
  #### Generate a heatmap to visualise the pairs correlation
  #### Seaborn
    import numpy as np
    mask = np.triu(np.ones_like(corrmat, dtype=np.bool))
    plt.figure(figsize=(50,50))
    g = sns.heatmap(corrmat,annot=False,cmap="RdYlGn", mask=mask, vmax=.3, center=0,square=True, linewidths=.5, 
                    cbar_kws={"shrink": .5})

  #### plotly
    import plotly.graph_objects as go
    
    # Create a mask to remove the upper triangle
    mask = np.triu(np.ones_like(corrmat, dtype=bool))
    
    # Apply the mask to the correlation matrix and flatten it
    corrmat_masked = corrmat.where(~mask)
    
    # Create the heatmap
    fig = go.Figure(data=go.Heatmap(
                       z=corrmat_masked,
                       x=corrmat.columns,
                       y=corrmat.columns,
                       hoverongaps = False,
                       colorscale="RdYlGn"))
    fig.update_layout(title='Customer Churn Analysis - Correlation Heatmap',
                      xaxis=dict(title='Features'),
                      yaxis=dict(title='Features'))
    
    # Show the plot
    fig.show()

Четыре шага, описанные в этом руководстве, дают ценную информацию для понимания оттока клиентов. Эти шаги являются важными компонентами исследовательского анализа данных (EDA), а также составляют неотъемлемую часть процесса XGBoost.

Исследовательский анализ данных (EDA) помогает выявлять закономерности, взаимосвязи и аномалии в данных, позволяя компаниям активно решать проблему оттока клиентов. Изучая набор данных, очищая данные, выполняя описательную статистику, визуализируя данные и проводя корреляционный анализ, мы получаем полное представление о факторах, влияющих на отток.

Основываясь на части 1, часть 2 этого руководства продолжит изучение обучения и проверки модели XGBoost. Кроме того, я представлю краткий обзор ключевых концепций, чтобы обеспечить всестороннее понимание оценки и проверки модели.

К концу всего руководства, объединяющего части 1 и 2, вы будете обладать необходимыми навыками для эффективного использования модели XGBoost. Это позволит вам прогнозировать отток клиентов и получать ценную информацию для принятия решений о продажах, направленных на повышение удержания клиентов.

Если вы нашли эту статью полезной, я был бы признателен, если бы вы выразили свою любовь несколькими хлопками в нижней части страницы. Не стесняйтесь оставлять свои идеи или отзывы в комментариях, а также не стесняйтесь передавать их своим друзьям и подписываться на меня на Medium.

Некоторые другие статьи Цзин Чена

Хотите подключиться?