Created
November 14, 2025 11:19
-
-
Save MikyPo/e4c19281c5612251e165f1dd5f62b9d0 to your computer and use it in GitHub Desktop.
rfm_analytics
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Developed by MikyPo | |
| # More code for DA here: https://dzen.ru/mikypo | |
| # Импорт библиотек | |
| import pandas as pd | |
| import numpy as np | |
| import openpyxl | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| from plotly.subplots import make_subplots | |
| from datetime import datetime | |
| from openpyxl.styles import Font, PatternFill, Alignment, Border, Side | |
| from openpyxl.formatting.rule import ColorScaleRule | |
| # Чтение файла | |
| df = pd.read_excel('sales.xlsx') | |
| df.info() | |
| # date_ship (datetime64[ns]) - дата отгрузки | |
| # id_head (object) - идентификатор клиента | |
| # dir (object) - направление бизнеса | |
| # quantity (float64) - количество отгруженного товара | |
| # ship_sum (float64) - стоимость отгруженного товара | |
| # Класс RFMAnalysis | |
| class RFMAnalysis: | |
| def __init__(self, df, analysis_date='2025-11-11'): | |
| """ | |
| Инициализация RFM анализа | |
| Parameters: | |
| df - DataFrame с данными | |
| analysis_date - дата анализа (по умолчанию 2025-11-11) | |
| """ | |
| self.df = df.copy() | |
| self.analysis_date = pd.to_datetime(analysis_date) | |
| self.rfm_data = None | |
| self.all_scores = {} | |
| self.all_segment_data = {} | |
| def prepare_data(self, direction=None): | |
| """Подготовка данных для анализа""" | |
| # Фильтрация по дате. Период анализа данных | |
| mask = (self.df['date_ship'] >= '2024-01-01') & (self.df['date_ship'] <= self.analysis_date) | |
| df_filtered = self.df[mask].copy() | |
| # Фильтрация по направлению бизнеса если указано. | |
| if direction: | |
| df_filtered = df_filtered[df_filtered['dir'] == direction] | |
| # Для Recency и Frequency используем только ship_sum > 0 чтобы не учитывать корректировки бухгалтерии, т.к. это не отгрузки | |
| df_rf = df_filtered[df_filtered['ship_sum'] > 0].copy() | |
| return df_filtered, df_rf | |
| def calculate_rfm_metrics(self, direction=None): | |
| """Расчёт RFM метрик""" | |
| df_filtered, df_rf = self.prepare_data(direction) | |
| # Recency: дни с последней покупки | |
| recency_data = df_rf.groupby('id_head')['date_ship'].max().apply( | |
| lambda x: (self.analysis_date - x).days | |
| ) | |
| # Frequency: количество уникальных дней с покупками | |
| frequency_data = df_rf.groupby('id_head')['date_ship'].nunique() | |
| # Monetary: общая сумма всех покупок (из всех данных, также с корректировками бухгалтерии) | |
| monetary_data = df_filtered.groupby('id_head')['ship_sum'].sum() | |
| # Собираем все метрики в один DataFrame и сбрасываем индекс, чтобы id_head стал колонкой | |
| rfm_metrics = pd.DataFrame({ | |
| 'recency': recency_data, | |
| 'frequency': frequency_data, | |
| 'monetary': monetary_data | |
| }).reset_index() | |
| # Заполнение пропусков нулями для клиентов, у которых нет покупок с ship_sum > 0 | |
| rfm_metrics = rfm_metrics.fillna({'recency': 999, 'frequency': 0, 'monetary': 0}) | |
| return rfm_metrics | |
| def calculate_scores(self, rfm_metrics): | |
| """Расчет баллов R, F, M""" | |
| scores = rfm_metrics.copy() | |
| # Убедимся, что нет нулевых или отрицательных значений | |
| scores = scores[scores['frequency'] >= 0] | |
| scores = scores[scores['monetary'] >= 0] | |
| # Recency: 1-лучшие (недавно), 2-средние, 3-худшие (давно) | |
| if len(scores) > 0: | |
| recency_33 = np.percentile(scores['recency'], 33) | |
| recency_67 = np.percentile(scores['recency'], 67) | |
| conditions_recency = [ | |
| scores['recency'] <= recency_33, | |
| (scores['recency'] > recency_33) & (scores['recency'] <= recency_67), | |
| scores['recency'] > recency_67 | |
| ] | |
| choices_recency = [1, 2, 3] | |
| scores['R'] = np.select(conditions_recency, choices_recency, default=3) | |
| else: | |
| scores['R'] = 3 | |
| # Frequency: 1-худшие (редко), 2-средние, 3-лучшие (часто) | |
| if len(scores) > 0: | |
| frequency_33 = np.percentile(scores['frequency'], 33) | |
| frequency_67 = np.percentile(scores['frequency'], 67) | |
| conditions_frequency = [ | |
| scores['frequency'] <= frequency_33, | |
| (scores['frequency'] > frequency_33) & (scores['frequency'] <= frequency_67), | |
| scores['frequency'] > frequency_67 | |
| ] | |
| choices_frequency = [1, 2, 3] | |
| scores['F'] = np.select(conditions_frequency, choices_frequency, default=1) | |
| else: | |
| scores['F'] = 1 | |
| # Monetary: 1-худшие (мало), 2-средние, 3-лучшие (много) | |
| if len(scores) > 0: | |
| monetary_33 = np.percentile(scores['monetary'], 33) | |
| monetary_67 = np.percentile(scores['monetary'], 67) | |
| conditions_monetary = [ | |
| scores['monetary'] <= monetary_33, | |
| (scores['monetary'] > monetary_33) & (scores['monetary'] <= monetary_67), | |
| scores['monetary'] > monetary_67 | |
| ] | |
| choices_monetary = [1, 2, 3] | |
| scores['M'] = np.select(conditions_monetary, choices_monetary, default=1) | |
| else: | |
| scores['M'] = 1 | |
| return scores | |
| def assign_segments(self, scores): | |
| """Назначение RFM-сегментов""" | |
| segments = [] | |
| for _, row in scores.iterrows(): | |
| r, f, m = row['R'], row['F'], row['M'] | |
| if r == 1 and f == 3 and m == 3: | |
| segment = "Champions" | |
| elif r == 1 and f >= 2 and m >= 2: | |
| segment = "Loyal Customers" | |
| elif r == 1: | |
| segment = "New Customers" | |
| elif r == 2 and f >= 2 and m >= 2: | |
| segment = "Potential Loyalists" | |
| elif r == 3 and f >= 2 and m >= 2: | |
| segment = "At Risk" | |
| elif r == 3 and f == 1 and m == 1: | |
| segment = "Lost Customers" | |
| else: | |
| segment = "Need Attention" | |
| segments.append(segment) | |
| scores['segment'] = segments | |
| scores['RFM'] = scores['R'].astype(str) + '-' + scores['F'].astype(str) + '-' + scores['M'].astype(str) | |
| return scores | |
| def analyze_direction(self, direction, show_plots=True, show_report=True): | |
| """Полный анализ для одного направления""" | |
| if show_report: | |
| print(f"\n{'='*60}") | |
| print(f"АНАЛИЗ ДЛЯ НАПРАВЛЕНИЯ: {direction}") | |
| print(f"{'='*60}") | |
| try: | |
| # Расчёт метрик | |
| rfm_metrics = self.calculate_rfm_metrics(direction) | |
| if show_report: | |
| print(f"Рассчитано метрик для {len(rfm_metrics)} клиентов") | |
| scores = self.calculate_scores(rfm_metrics) | |
| if show_report: | |
| print(f"Рассчитано баллов для {len(scores)} клиентов") | |
| scores = self.assign_segments(scores) | |
| if show_report: | |
| print(f"Назначены сегменты для {len(scores)} клиентов") | |
| # Всегда рассчитываем segment_data, даже если не показываем графики | |
| segment_data = scores.groupby('segment').agg({ | |
| 'monetary': ['sum', 'count'], | |
| 'recency': 'mean', | |
| 'frequency': 'mean' | |
| }).round(2) | |
| segment_data.columns = ['revenue', 'clients_count', 'avg_recency', 'avg_frequency'] | |
| segment_data = segment_data.reset_index() | |
| # Визуализации (только если нужно показывать) | |
| if show_plots: | |
| self._create_visualizations(scores, direction, segment_data) | |
| # Отчет (только если нужно показывать) | |
| if show_report: | |
| report = self._generate_report(scores, direction, segment_data) | |
| else: | |
| report = None | |
| # Добавляем направление в данные | |
| scores['direction'] = direction | |
| # Сохраняем результаты для последующего сравнения | |
| self.all_scores[direction] = scores | |
| self.all_segment_data[direction] = segment_data | |
| return scores, segment_data, report | |
| except Exception as e: | |
| print(f"Ошибка при анализе направления {direction}: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return None, None, None | |
| def _create_visualizations(self, scores, direction, segment_data=None): | |
| """Создание визуализаций для направления (внутренний метод)""" | |
| # Если segment_data не передан, рассчитываем его | |
| if segment_data is None: | |
| segment_data = scores.groupby('segment').agg({ | |
| 'monetary': ['sum', 'count'], | |
| 'recency': 'mean', | |
| 'frequency': 'mean' | |
| }).round(2) | |
| segment_data.columns = ['revenue', 'clients_count', 'avg_recency', 'avg_frequency'] | |
| segment_data = segment_data.reset_index() | |
| # 1. ГРАФИК: Круговая диаграмма: Распределение выручки по RFM-сегментам | |
| if len(segment_data) > 0: | |
| fig1 = px.pie(segment_data, | |
| values='revenue', | |
| names='segment', | |
| title=f'{direction}: Распределение выручки по RFM-сегментам', | |
| hole=0.4) | |
| fig1.update_traces(textposition='inside', textinfo='percent+label') | |
| fig1.show() | |
| # 2. ГРАФИК: Сравнение сегментов: оборот и количество клиентов | |
| if len(segment_data) > 0: | |
| fig2 = make_subplots(specs=[[{"secondary_y": True}]]) | |
| fig2.add_trace( | |
| go.Bar(x=segment_data['segment'], y=segment_data['revenue']/1e6, | |
| name="Оборот (млн ₽)", marker_color='blue'), | |
| secondary_y=False, | |
| ) | |
| fig2.add_trace( | |
| go.Scatter(x=segment_data['segment'], y=segment_data['clients_count'], | |
| name="Количество клиентов", marker_color='red', mode='lines+markers'), | |
| secondary_y=True, | |
| ) | |
| fig2.update_layout( | |
| title_text=f"{direction}: Сравнение сегментов - оборот и количество клиентов", | |
| xaxis_title="RFM-сегменты" | |
| ) | |
| fig2.update_yaxes(title_text="Оборот (млн ₽)", secondary_y=False) | |
| fig2.update_yaxes(title_text="Количество клиентов", secondary_y=True) | |
| fig2.show() | |
| # 3. ГРАФИК: Средние метрики по направлению между сегментами | |
| if len(segment_data) > 0: | |
| fig3 = go.Figure(data=[ | |
| go.Bar(name='Средний Recency', x=segment_data['segment'], y=segment_data['avg_recency']), | |
| go.Bar(name='Средняя Frequency', x=segment_data['segment'], y=segment_data['avg_frequency']) | |
| ]) | |
| fig3.update_layout( | |
| title=f"{direction}: Средние метрики по сегментам", | |
| xaxis_title="RFM-сегменты", | |
| yaxis_title="Значения метрик", | |
| barmode='group' | |
| ) | |
| fig3.show() | |
| # 4. ГРАФИК: Распределение клиентов по сегментам | |
| if len(segment_data) > 0: | |
| fig4 = px.bar(segment_data, | |
| x='segment', | |
| y='clients_count', | |
| title=f'{direction}: Распределение уникальных клиентов по RFM-сегментам', | |
| labels={'segment': 'RFM-сегмент', 'clients_count': 'Количество клиентов'}, | |
| text='clients_count', | |
| color='segment') | |
| fig4.update_traces(texttemplate='%{text}', textposition='outside') | |
| fig4.update_layout(showlegend=False, xaxis_tickangle=45) | |
| fig4.show() | |
| # 5. ГРАФИК: Доля клиентов по сегментам | |
| if len(segment_data) > 0: | |
| total_clients = segment_data['clients_count'].sum() | |
| segment_data['percentage'] = (segment_data['clients_count'] / total_clients * 100).round(1) | |
| fig5 = px.pie(segment_data, | |
| values='clients_count', | |
| names='segment', | |
| title=f'{direction}: Доля клиентов по RFM-сегментам', | |
| hole=0.4) | |
| fig5.update_traces(textposition='inside', textinfo='percent+label+value') | |
| fig5.show() | |
| return segment_data | |
| def _generate_report(self, scores, direction, segment_data): | |
| """Генерация отчета по направлению (внутренний метод)""" | |
| total_revenue = scores['monetary'].sum() / 1e6 | |
| total_clients = len(scores) | |
| champions_data = segment_data[segment_data['segment'] == 'Champions'] | |
| if not champions_data.empty: | |
| champions_revenue = champions_data['revenue'].iloc[0] / 1e6 | |
| champions_clients = champions_data['clients_count'].iloc[0] | |
| champions_percent = (champions_revenue / total_revenue) * 100 if total_revenue > 0 else 0 | |
| else: | |
| champions_revenue = 0 | |
| champions_clients = 0 | |
| champions_percent = 0 | |
| lost_data = segment_data[segment_data['segment'] == 'Lost Customers'] | |
| if not lost_data.empty: | |
| lost_clients = lost_data['clients_count'].iloc[0] | |
| lost_percent = (lost_clients / total_clients) * 100 if total_clients > 0 else 0 | |
| else: | |
| lost_clients = 0 | |
| lost_percent = 0 | |
| report = f""" | |
| ОТЧЕТ ДЛЯ НАПРАВЛЕНИЯ: {direction} | |
| {'='*50} | |
| ОБЩАЯ СТАТИСТИКА: | |
| Общая выручка: {total_revenue:.1f} млн ₽ | |
| Всего клиентов: {total_clients} | |
| Выручка от Champions: {champions_revenue:.1f} млн ₽ ({champions_percent:.1f}%) | |
| РАСПРЕДЕЛЕНИЕ СЕГМЕНТОВ: | |
| """ | |
| for segment in segment_data.itertuples(): | |
| revenue_percent = (segment.revenue / 1e6 / total_revenue) * 100 if total_revenue > 0 else 0 | |
| clients_percent = (segment.clients_count / total_clients) * 100 if total_clients > 0 else 0 | |
| report += f"{segment.segment}: {segment.clients_count} клиентов ({clients_percent:.1f}%), {segment.revenue/1e6:.1f} млн ₽ ({revenue_percent:.1f}%)\n" | |
| report += f""" | |
| РЕКОМЕНДАЦИИ ДЛЯ {direction}: | |
| """ | |
| if champions_percent > 50: | |
| report += "Отличная концентрация! Champions приносят более 50% выручки\n" | |
| report += "Усилить программы лояльности для Champions\n" | |
| else: | |
| report += "Необходимо увеличить долю Champions в выручке\n" | |
| report += f"Потеряно {lost_percent:.1f}% клиентов\n" | |
| report += "Проанализировать причины оттока\n" | |
| print(report) | |
| return report | |
| def analyze_multiple_directions(self, directions, show_plots=True, show_reports=True): | |
| """Анализ нескольких направлений без вывода сравнения""" | |
| print("🚀 Запуск анализа направлений...") | |
| for direction in directions: | |
| self.analyze_direction(direction, show_plots=show_plots, show_report=show_reports) | |
| print(f"✅ Анализ завершен для {len(self.all_scores)} направлений") | |
| return self.all_scores, self.all_segment_data | |
| def compare_directions(self): | |
| """Сравнительная аналитика по всем проанализированным направлениям""" | |
| if len(self.all_scores) < 2: | |
| print(f"⚠️ Для сравнения нужно минимум 2 направления. Сейчас проанализировано: {len(self.all_scores)}") | |
| return None | |
| print(f"\n{'='*80}") | |
| print("📊 СРАВНИТЕЛЬНАЯ АНАЛИТИКА ПО НАПРАВЛЕНИЯМ") | |
| print(f"{'='*80}") | |
| # Объединяем все данные | |
| combined_data = pd.concat([scores for scores in self.all_scores.values() if scores is not None], ignore_index=True) | |
| # 1. Распределение клиентов по RFM-сегментам в разных направлениях | |
| segment_dist = pd.crosstab(combined_data['direction'], combined_data['segment']) | |
| fig1 = px.bar(segment_dist, | |
| title="Распределение клиентов по RFM-сегментам в разных направлениях", | |
| labels={'value': 'Количество клиентов', 'direction': 'Направление'}, | |
| barmode='group') | |
| fig1.show() | |
| # 2. Процентное распределение клиентов по RFM-сегментам | |
| segment_percent = segment_dist.div(segment_dist.sum(axis=1), axis=0) * 100 | |
| fig2 = px.bar(segment_percent, | |
| title="Процентное распределение клиентов по RFM-сегментам", | |
| labels={'value': 'Процент клиентов', 'direction': 'Направление'}, | |
| barmode='group') | |
| fig2.show() | |
| # 3. Сводная таблица по направлениям | |
| summary = combined_data.groupby('direction').agg({ | |
| 'monetary': ['sum', 'count'], | |
| 'recency': 'mean', | |
| 'frequency': 'mean' | |
| }).round(2) | |
| summary.columns = ['Выручка', 'Клиенты', 'Ср. Recency', 'Ср. Frequency'] | |
| summary['Выручка на клиента'] = (summary['Выручка'] / summary['Клиенты']).round(2) | |
| # Добавляем количество уникальных клиентов по сегментам | |
| champions_count = combined_data[combined_data['segment'] == 'Champions'].groupby('direction').size() | |
| loyal_count = combined_data[combined_data['segment'] == 'Loyal Customers'].groupby('direction').size() | |
| new_count = combined_data[combined_data['segment'] == 'New Customers'].groupby('direction').size() | |
| potential_count = combined_data[combined_data['segment'] == 'Potential Loyalists'].groupby('direction').size() | |
| at_risk_count = combined_data[combined_data['segment'] == 'At Risk'].groupby('direction').size() | |
| lost_count = combined_data[combined_data['segment'] == 'Lost Customers'].groupby('direction').size() | |
| need_attention_count = combined_data[combined_data['segment'] == 'Need Attention'].groupby('direction').size() | |
| summary['Champions'] = champions_count | |
| summary['Loyal Customers'] = loyal_count | |
| summary['New Customers'] = new_count | |
| summary['Potential Loyalists'] = potential_count | |
| summary['At Risk'] = at_risk_count | |
| summary['Lost Customers'] = lost_count | |
| summary['Need Attention'] = need_attention_count | |
| # Заполняем пропуски нулями | |
| summary = summary.fillna(0) | |
| print("📈 СВОДНАЯ ТАБЛИЦА ПО НАПРАВЛЕНИЯМ:") | |
| print(summary) | |
| # 4. Сравнение выручки по направлениям | |
| fig3 = px.bar(summary.reset_index(), | |
| x='direction', y='Выручка', | |
| title='Выручка по направлениям', | |
| labels={'direction': 'Направление', 'Выручка': 'Выручка'}) | |
| fig3.show() | |
| # 5. Сравнение количества клиентов | |
| fig4 = px.bar(summary.reset_index(), | |
| x='direction', y='Клиенты', | |
| title='Общее количество клиентов по направлениям', | |
| labels={'direction': 'Направление', 'Клиенты': 'Количество клиентов'}) | |
| fig4.show() | |
| # 6. Количество уникальных клиентов по сегментам (новый график) | |
| segment_counts = summary[['Champions', 'Loyal Customers', 'New Customers', | |
| 'Potential Loyalists', 'At Risk', 'Lost Customers', 'Need Attention']] | |
| fig5 = px.bar(segment_counts.reset_index(), | |
| x='direction', | |
| y=segment_counts.columns, | |
| title='Количество уникальных клиентов по сегментам и направлениям', | |
| labels={'direction': 'Направление', 'value': 'Количество клиентов', 'variable': 'Сегмент'}, | |
| barmode='group') | |
| fig5.show() | |
| # 7. Процентное распределение клиентов по сегментам (столбчатая диаграмма 100%) | |
| segment_percent_total = segment_counts.div(segment_counts.sum(axis=1), axis=0) * 100 | |
| fig6 = px.bar(segment_percent_total.reset_index(), | |
| x='direction', | |
| y=segment_percent_total.columns, | |
| title='Процентное распределение клиентов по сегментам (100% stacked)', | |
| labels={'direction': 'Направление', 'value': 'Процент клиентов', 'variable': 'Сегмент'}, | |
| barmode='relative') | |
| fig6.update_yaxes(title_text='Процент клиентов (%)') | |
| fig6.show() | |
| # 8. Сравнение средних метрик по направлениям | |
| metrics_comparison = summary[['Ср. Recency', 'Ср. Frequency', 'Выручка на клиента']].reset_index() | |
| fig7 = make_subplots(rows=1, cols=3, | |
| subplot_titles=('Средний Recency', 'Средняя Frequency', 'Выручка на клиента')) | |
| fig7.add_trace(go.Bar(x=metrics_comparison['direction'], y=metrics_comparison['Ср. Recency']), | |
| row=1, col=1) | |
| fig7.add_trace(go.Bar(x=metrics_comparison['direction'], y=metrics_comparison['Ср. Frequency']), | |
| row=1, col=2) | |
| fig7.add_trace(go.Bar(x=metrics_comparison['direction'], y=metrics_comparison['Выручка на клиента']), | |
| row=1, col=3) | |
| fig7.update_layout(title_text='Сравнение средних метрик по направлениям', showlegend=False) | |
| fig7.show() | |
| return summary | |
| def analyze_clients_distribution(self, direction): | |
| """Детальный анализ распределения клиентов для одного направления""" | |
| if direction not in self.all_scores: | |
| print(f"⚠️ Направление {direction} не проанализировано. Сначала выполните analyze_direction()") | |
| return None | |
| scores = self.all_scores[direction] | |
| segment_data = self.all_segment_data.get(direction) | |
| if segment_data is None: | |
| segment_data = scores.groupby('segment').agg({ | |
| 'monetary': ['sum', 'count'], | |
| 'recency': 'mean', | |
| 'frequency': 'mean' | |
| }).round(2) | |
| segment_data.columns = ['revenue', 'clients_count', 'avg_recency', 'avg_frequency'] | |
| segment_data = segment_data.reset_index() | |
| print(f"\n{'='*60}") | |
| print(f"📊 АНАЛИЗ КЛИЕНТОВ ДЛЯ НАПРАВЛЕНИЯ: {direction}") | |
| print(f"{'='*60}") | |
| total_clients = len(scores) | |
| print(f"Всего уникальных клиентов: {total_clients}") | |
| # 1. Столбчатая диаграмма распределения клиентов | |
| fig1 = px.bar(segment_data, | |
| x='segment', | |
| y='clients_count', | |
| title=f'{direction}: Распределение клиентов по RFM-сегментам', | |
| labels={'segment': 'RFM-сегмент', 'clients_count': 'Количество клиентов'}, | |
| text='clients_count', | |
| color='segment') | |
| fig1.update_traces(texttemplate='%{text}', textposition='outside') | |
| fig1.update_layout(showlegend=False, xaxis_tickangle=45) | |
| fig1.show() | |
| # 2. Круговая диаграмма долей клиентов | |
| segment_data['percentage'] = (segment_data['clients_count'] / total_clients * 100).round(1) | |
| fig2 = px.pie(segment_data, | |
| values='clients_count', | |
| names='segment', | |
| title=f'{direction}: Доля клиентов по RFM-сегментам', | |
| hole=0.4) | |
| fig2.update_traces(textposition='inside', textinfo='percent+label+value') | |
| fig2.show() | |
| # 3. Сравнение количества клиентов и выручки | |
| fig3 = make_subplots(specs=[[{"secondary_y": True}]]) | |
| fig3.add_trace( | |
| go.Bar(x=segment_data['segment'], y=segment_data['clients_count'], | |
| name="Количество клиентов", marker_color='lightblue'), | |
| secondary_y=False, | |
| ) | |
| fig3.add_trace( | |
| go.Scatter(x=segment_data['segment'], y=segment_data['revenue']/1e6, | |
| name="Выручка (млн ₽)", marker_color='red', mode='lines+markers'), | |
| secondary_y=True, | |
| ) | |
| fig3.update_layout( | |
| title_text=f"{direction}: Сравнение количества клиентов и выручки по сегментам", | |
| xaxis_title="RFM-сегменты" | |
| ) | |
| fig3.update_yaxes(title_text="Количество клиентов", secondary_y=False) | |
| fig3.update_yaxes(title_text="Выручка (млн ₽)", secondary_y=True) | |
| fig3.show() | |
| # 4. Топ сегменты по количеству клиентов | |
| top_segments = segment_data.nlargest(5, 'clients_count')[['segment', 'clients_count', 'percentage']] | |
| fig4 = px.bar(top_segments, | |
| x='segment', | |
| y='clients_count', | |
| title=f'{direction}: Топ-5 сегментов по количеству клиентов', | |
| labels={'segment': 'RFM-сегмент', 'clients_count': 'Количество клиентов'}, | |
| text='percentage', | |
| color='segment') | |
| fig4.update_traces(texttemplate='%{text}%', textposition='outside') | |
| fig4.update_layout(showlegend=False) | |
| fig4.show() | |
| # Вывод статистики | |
| print("\n📈 СТАТИСТИКА ПО КЛИЕНТАМ:") | |
| print("-" * 40) | |
| for _, row in segment_data.iterrows(): | |
| print(f"{row['segment']}: {row['clients_count']} клиентов ({row['percentage']}%)") | |
| champions = segment_data[segment_data['segment'] == 'Champions'] | |
| if not champions.empty: | |
| print(f"\n🎯 Champions: {champions['clients_count'].iloc[0]} клиентов ({champions['percentage'].iloc[0]}%)") | |
| lost = segment_data[segment_data['segment'] == 'Lost Customers'] | |
| if not lost.empty: | |
| print(f"⚠️ Lost Customers: {lost['clients_count'].iloc[0]} клиентов ({lost['percentage'].iloc[0]}%)") | |
| return segment_data | |
| def save_to_excel(self, filename='rfm_analysis_results.xlsx'): | |
| """Сохранение всех результатов в Excel""" | |
| if not self.all_scores: | |
| print("⚠️ Нет данных для сохранения") | |
| return | |
| try: | |
| with pd.ExcelWriter(filename) as writer: | |
| # Все клиенты | |
| all_clients_data = pd.concat(self.all_scores.values(), ignore_index=True) | |
| all_clients_data.to_excel(writer, sheet_name='Все клиенты', index=False) | |
| # По направлениям | |
| for direction, scores in self.all_scores.items(): | |
| sheet_name = f'{direction}'[:31] # Ограничение длины имени листа | |
| # Переупорядочиваем колонки, чтобы id_head был первой | |
| columns_order = ['id_head', 'direction', 'recency', 'frequency', 'monetary', | |
| 'R', 'F', 'M', 'RFM', 'segment'] | |
| # Добавляем отсутствующие колонки | |
| for col in columns_order: | |
| if col not in scores.columns: | |
| columns_order.remove(col) | |
| scores_reordered = scores[columns_order] | |
| scores_reordered.to_excel(writer, sheet_name=sheet_name, index=False) | |
| # Сводная таблица по направлениям | |
| if len(self.all_scores) >= 1: | |
| combined_data = pd.concat(self.all_scores.values(), ignore_index=True) | |
| summary = combined_data.groupby('direction').agg({ | |
| 'monetary': ['sum', 'count'], | |
| 'recency': 'mean', | |
| 'frequency': 'mean' | |
| }).round(2) | |
| summary.columns = ['Выручка', 'Клиенты', 'Ср. Recency', 'Ср. Frequency'] | |
| summary['Выручка на клиента'] = (summary['Выручка'] / summary['Клиенты']).round(2) | |
| summary.to_excel(writer, sheet_name='Сводная') | |
| # Сегменты по направлениям (только если есть данные) | |
| valid_segment_data = {} | |
| for direction, segment_data in self.all_segment_data.items(): | |
| if segment_data is not None and len(segment_data) > 0: | |
| valid_segment_data[direction] = segment_data | |
| if valid_segment_data: | |
| try: | |
| segment_summary = pd.concat( | |
| valid_segment_data.values(), | |
| keys=valid_segment_data.keys() | |
| ) | |
| segment_summary.to_excel(writer, sheet_name='Сегменты') | |
| except Exception as e: | |
| print(f"⚠️ Не удалось сохранить сегменты: {e}") | |
| # Сохраняем каждый сегмент отдельно | |
| for direction, segment_data in valid_segment_data.items(): | |
| sheet_name = f'Сегменты_{direction}'[:31] | |
| segment_data.to_excel(writer, sheet_name=sheet_name, index=False) | |
| print(f"✅ Все данные сохранены в файл: {filename}") | |
| print(f"📊 Сохранено клиентов: {len(all_clients_data)}") | |
| print(f"🎯 Направления: {list(self.all_scores.keys())}") | |
| except Exception as e: | |
| print(f"❌ Ошибка при сохранении файла: {e}") | |
| def save_to_excel_simple(self, filename='rfm_analysis_results.xlsx'): | |
| """Упрощенное сохранение результатов в Excel""" | |
| if not self.all_scores: | |
| print("⚠️ Нет данных для сохранения") | |
| return | |
| try: | |
| with pd.ExcelWriter(filename) as writer: | |
| # Все клиенты | |
| all_clients_data = pd.concat(self.all_scores.values(), ignore_index=True) | |
| all_clients_data.to_excel(writer, sheet_name='Все клиенты', index=False) | |
| # По направлениям | |
| for direction, scores in self.all_scores.items(): | |
| sheet_name = f'{direction}'[:31] | |
| scores.to_excel(writer, sheet_name=sheet_name, index=False) | |
| print(f"✅ Основные данные сохранены в файл: {filename}") | |
| print(f"📊 Сохранено клиентов: {len(all_clients_data)}") | |
| except Exception as e: | |
| print(f"❌ Ошибка при сохранении файла: {e}") | |
| # Инициализация анализатора | |
| rfm_analyzer = RFMAnalysis(df) | |
| print("✅ RFM анализатор инициализирован") | |
| # Анализ одного направления 'some1' с полным выводом | |
| direction = 'some1' | |
| scores, segment_data, report = rfm_analyzer.analyze_direction(direction, show_plots=True, show_report=True) | |
| # Дополнительный анализ клиентов одного направления 'some1' | |
| rfm_analyzer.analyze_clients_distribution(direction) | |
| # Анализ нескольких направлений с выводом графиков по направлениям | |
| # directions = ['some1', 'some2'] | |
| # for direction in directions: | |
| # scores, segment_data, report = rfm_analyzer.analyze_direction(direction, show_plots=True, show_report=True) | |
| # Быстрый анализ всех направлений без вывода графиков по направлениям + сравнение | |
| directions = ['some1', 'some2'] | |
| # Анализ без визуализаций (быстро) | |
| all_scores, all_segments = rfm_analyzer.analyze_multiple_directions( | |
| directions, | |
| show_plots=False, | |
| show_reports=False | |
| ) | |
| # Сравнительная аналитика | |
| summary = rfm_analyzer.compare_directions() | |
| # Сохранение результатов в Excel | |
| rfm_analyzer.save_to_excel('rfm_analysis_results.xlsx') | |
| # Просмотр результатов | |
| print("📊 ПРОСМОТР РЕЗУЛЬТАТОВ:") | |
| print(f"Проанализировано направлений: {len(rfm_analyzer.all_scores)}") | |
| print(f"Направления: {list(rfm_analyzer.all_scores.keys())}") | |
| if rfm_analyzer.all_scores: | |
| direction = list(rfm_analyzer.all_scores.keys())[0] | |
| print(f"\nПример данных для направления {direction}:") | |
| display(rfm_analyzer.all_scores[direction].head()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment