Week 13 Unique Paths

独特路径算法解析
本文探讨了一个经典的动态规划问题——寻找网格中从左上角到右下角的不同路径数量。通过分析机器人仅能向下或向右移动的条件,我们建立了状态方程,并提供了详细的算法实现,包括初始化边界条件和迭代计算过程。

62.Unique Paths

问题概述

A robot is located at the top-left corner of a m x n grid (marked ‘Start’ in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked ‘Finish’ in the diagram below).

How many possible unique paths are there?
在这里插入图片描述

Note: m and n will be at most 100.

Example 1:
Input: m = 3, n = 2
Output: 3
Explanation:
From the top-left corner, there are a total of 3 ways to reach the bottom-right corner:
1. Right -> Right -> Down
2. Right -> Down -> Right
3. Down -> Right -> Right
Example 2
Input: m = 7, n = 3
Output: 28

分析

这是一道基本动态规划问题。
机器人只能向下或向右移动,当它位于一个坐标点时,有两种可能:

1.It arrives at that point from above (moving down to that point);
2.It arrives at that point from left (moving right to that point).

假设到达一个点(i,j)的路径数用P[i][j]表示,
因而可以建立状态方程:
P[i][j]=P[i-1][j]+P[i][j-1]
P[0][j] = 1, P[i][0] = 1(初态)

代码

/c++/
class Solution {
    int uniquePaths(int m, int n) {
        vector<vector<int> > path(m, vector<int> (n, 1));
        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                path[i][j] = path[i - 1][j] + path[i][j - 1];
        return path[m - 1][n - 1];
    }
};
class SYCNMSpider: def __init__(self, gradio_app, mapping_path): self.__gr = gradio_app # Gradio 应用 self.mapping_flows_data = self.init_mapping_flows(mapping_path) self.__paser_tools = ParserTools(gradio_app) @staticmethod def init_mapping_flows(flows_path): """ 初始化映射表 """ df = pd.read_excel(flows_path, header=[0, 1, 2]) # 填充空值 df.fillna(method='ffill', inplace=True) new_flows = df['新旧版流量对照']['新版'] old_flows = df['新旧版流量对照']['旧版'] mapping_flows = [] for x, y in zip(new_flows.values, old_flows.values): # 映射数据 new_flows = {"level_one_flows": x[0], "level_two_flows": x[1], "level_three_flows": x[2], "leaf_flows": x[3]} old_flows = {"level_one_flows": y[0], "level_two_flows": y[1], "level_three_flows": y[2], "leaf_flows": y[3]} # 添加到映射表 mapping_flows.append({"new_flows": new_flows, "old_flows": old_flows}) return mapping_flows @staticmethod def seach_mapping_flows(mapping_flows, res_return=False, leaf_flows_name="", mapping_new_or_old="new_flows"): """ 搜索映射表 :param flows_level: 流量等级 :param flows_name: 流量名称 :param mapping_new_or_old: 映射新旧版 """ mapping_new_or_old_all = {"new_flows": "old_flows", "old_flows": "new_flows"} if mapping_new_or_old not in mapping_new_or_old_all: raise ValueError("mapping_new_or_old must be in mapping_new_or_old_all 'new_flows' or 'old_flows'") return_flows = mapping_new_or_old if res_return: return_flows = mapping_new_or_old_all[mapping_new_or_old] result = [] for mapping in mapping_flows: if mapping_new_or_old == "new_flows": if mapping["new_flows"]["leaf_flows"] == leaf_flows_name: result.append(mapping[return_flows]) elif mapping_new_or_old == "old_flows": if mapping["old_flows"]["leaf_flows"] == leaf_flows_name: result.append(mapping[return_flows]) # 去重 seen = set() unique_list = [] for d in result: # frozenset将字典条目转换为不可变集合 tuple_d = frozenset(d.items()) if tuple_d not in seen: seen.add(tuple_d) unique_list.append(d) return unique_list def update_file_component(self, choice): if choice == "多文件模式": return self.__gr.File(file_count="multiple", file_types=['.xlsx', '.xls']) elif choice == "文件夹模式": return self.__gr.File(file_count="directory") def select_level(self, x, level="level_one_flows"): result_list = self.seach_mapping_flows(self.mapping_flows_data, res_return=False, leaf_flows_name=x, mapping_new_or_old="new_flows") return result_list[0][level] def start_app(self, input_paths: list): check_prefix = ["商品", "店铺", "直播", "内容"] all_data = [] for file in input_paths: filename = os.path.basename(file) try: x = filename.split("_") y = x[1].split("-") prefix_name = x[0] if prefix_name not in check_prefix: raise self.__gr.Error(f"文件名错误无法判断来源!请添加前缀(例如:商品_)") prefix_date = "-".join(y[1::]) excel_file = pd.ExcelFile(file) sheet_name_list = ['无线流量来源', '经营优势来源渠道'] one_level = '经营优势' sheet_names = excel_file.sheet_names # --------------------------------------------------------------------------------------------------------------- # 读取所有工作表 dfs = {sheet_name: pd.read_excel(excel_file, sheet_name=sheet_name, skiprows=5) for sheet_name in sheet_names} flow_df = dfs[sheet_name_list[0]] # 获取 无线流量来源 # 剔除 经营优势 一级来源 # flow_df = flow_df[flow_df["一级来源"] != one_level] flow_df.insert(flow_df.shape[1], '日期', prefix_date) if len(sheet_names) > 1: # 非全站推 management_df = dfs[sheet_name_list[1]] # 获取经营优势来源渠道 # 添加新列数据 management_df.loc[:, '日期'] = prefix_date management_df.loc[:, '来源'] = prefix_name + "-非全站推" management_df.loc[:, '一级来源'] = one_level management_df.loc[:, '二级来源'] = one_level management_df['三级来源'] = management_df['来源名称'] flow_df.insert(flow_df.shape[1], '来源', prefix_name + "-非全站推") # 删除 来源名称 management_df.drop(columns=['来源名称'], inplace=True) concat_result = pd.concat([flow_df, management_df], axis=0, ignore_index=True) else: # 全站推 flow_df.insert(flow_df.shape[1], '来源', prefix_name + "-全站推") flow_df_new = flow_df[flow_df["二级来源"] != "汇总"] # 需要处理数据 need_parser_df = flow_df[flow_df["二级来源"] == "汇总"] need_parser_df['三级来源'] = need_parser_df['一级来源'] need_parser_df['一级来源'] = need_parser_df['三级来源'].apply(lambda x: self.select_level(x)) need_parser_df['二级来源'] = need_parser_df['三级来源'].apply( lambda x: self.select_level(x, level="level_two_flows")) concat_result = pd.concat([flow_df_new, need_parser_df], axis=0, ignore_index=True) # --------------------------------------------------------------------------------------------------------------- # 将新增加的 'date' 列移动到第一列 cols = ['日期', '来源'] + [col for col in concat_result.columns if col != '日期' and col != '来源'] all_data.append(concat_result[cols]) self.__gr.Info(f"解析成功文件名:{filename}") except self.__gr.exceptions.Error: raise self.__gr.Error(f"文件名错误无法判断来源!请添加前缀(例如:商品_)") except Exception as e: raise self.__gr.Error(f"解析失败,文件名:{filename}") filename = self.__paser_tools.get_save_filename("生意参谋_流量", "sycm_flows") data = pd.concat(all_data, axis=0, ignore_index=True) data.to_excel(filename, index=False, sheet_name="流量来源") return filename class SYCMDownload: def __init__(self, gradio_app, mapping_path): self.__gr = gradio_app # Gradio 应用 self.__paser_tools = ParserTools(gradio_app) self.__downloader = app_sycm_download.SYCMDownloadSpider() self.__date_map = {"日": "day", "月": "month", "周": "week", "近7天": "recent7", "近30天": "recent30"} self.mapping_flows_data = self.init_mapping_flows(mapping_path) def start_app(self, cookies, output_type, flows_dim: list, date_dim, start_date, end_date): all_data = [] all_path = [] logging.info( f" output_type: {output_type} flows_dim: {flows_dim} date_dim: {date_dim} start_date: {start_date} end_date: {end_date}") # 获取参数 dateType = self.__date_map[date_dim] # 获取cookies cookies = self.__paser_tools.parse_cookie(cookies) # 获取时间维度 if date_dim in ["日", "周", "月"]: dateRanges = self.paser_date(dateType, start_date, end_date) else: dateRanges = self.paser_date(dateType) dateType = "day" logging.info(f"dateRanges: {dateRanges}") progress = self.__gr.Progress(track_tqdm=True) for dateRange in progress.tqdm(dateRanges, desc="下载进度", unit="number"): logging.info(f"dateType: {dateType} dateRange: {dateRange}") status, data = self.__downloader.crawl(cookies=cookies, dateType=dateType, dateRange=dateRange) if not status: raise self.__gr.Error("采集失败!") byte_stream = io.BytesIO(data) excel_file = pd.ExcelFile(byte_stream) data = self.paser_flows_dim(excel_file, flows_dim, date_dim, dateRange) all_data.append(data) if output_type == "多个文件": name = "~".join(dateRange.split("|")) filename_m = self.__paser_tools.get_save_filename(f"生意参谋_流量_{name}", "sycm_flows_download") data.to_excel(filename_m, index=False, sheet_name="流量来源") all_path.append(filename_m) if output_type == "多个文件": return all_path filename = self.__paser_tools.get_save_filename("生意参谋_流量_下载", "sycm_flows_download") result = pd.concat(all_data, axis=0, ignore_index=True) result.to_excel(filename, index=False, sheet_name="流量来源") self.__state = False return [filename] def paser_flows_dim(self, excel_file_data, flows_dim, date_dim, dateRange): df = pd.read_excel(excel_file_data, skiprows=5) sheet_name_list = ['店铺渠道', '经营优势来源渠道'] # 全站推 promotion = ["整体", "非全站推广期", "全站推广期"] one_level = '经营优势' sheet_names = excel_file_data.sheet_names # 读取所有工作表 dfs = {sheet_name: pd.read_excel(excel_file_data, sheet_name=sheet_name, skiprows=5) for sheet_name in sheet_names} # 日期 prefix_date = dateRange.split("|")[0] if date_dim == "周" or date_dim == "月": x_1 = dateRange.split("|") prefix_date = "~".join(x_1) flow_df = dfs[sheet_name_list[0]] flow_df.insert(flow_df.shape[1], '日期', prefix_date) if "全站推广期" in list(flow_df.columns): flow_df_new = flow_df[(flow_df["二级来源"] != "汇总") & (flow_df["全站推广期"] == "全站推广期")] flow_df_not = flow_df[flow_df["全站推广期"] != "全站推广期"] # 需要处理数据 need_parser_df = flow_df[(flow_df["二级来源"] == "汇总") & (flow_df["全站推广期"] == "全站推广期")] need_parser_df['三级来源'] = need_parser_df['一级来源'] need_parser_df['一级来源'] = need_parser_df['三级来源'].apply(lambda x: self.select_level(x)) need_parser_df['二级来源'] = need_parser_df['三级来源'].apply( lambda x: self.select_level(x, level="level_two_flows")) result_data = pd.concat([flow_df_new, need_parser_df, flow_df_not], axis=0, ignore_index=True) else: flow_df.loc[:, '全站推广期'] = "整体" result_data = flow_df if len(sheet_names) > 1: # 处理经营优势 management_df = dfs[sheet_name_list[1]] management_df.loc[:, '全站推广期'] = "整体" management_df.loc[:, '日期'] = prefix_date management_df.loc[:, '一级来源'] = one_level management_df.loc[:, '二级来源'] = one_level management_df['三级来源'] = management_df['来源名称'] management_df.drop(columns=['来源名称'], inplace=True) result = pd.concat([result_data, management_df], axis=0, ignore_index=True) else: result = result_data # 重新排序列名 cols = ['日期', '流量载体'] + [col for col in result.columns if col != '日期' and col != '流量载体'] new_flow_dim = [dim + "流量" for dim in flows_dim] filter_cols = result[cols] return filter_cols[filter_cols['流量载体'].isin(new_flow_dim)] def select_level(self, x, level="level_one_flows"): result_list = self.seach_mapping_flows(self.mapping_flows_data, res_return=False, leaf_flows_name=x, mapping_new_or_old="new_flows") return result_list[0][level] @staticmethod def paser_date(time_type, start_date=None, end_date=None): today = datetime.datetime.today() end_day = today - datetime.timedelta(days=1) date_format = "%Y-%m-%d" def parse_date(date_str): return datetime.datetime.strptime(date_str, date_format) if time_type in ["day", "week", "month"]: if not start_date or not end_date: raise ValueError("Error: Start date and end date must be provided for day, week, and month types.") start_dt = parse_date(start_date) end_dt = parse_date(end_date) if end_dt > end_day: raise ValueError("Error: End date cannot be later than the day before the current date.") if time_type == "week": if start_dt.weekday() != 0 or end_dt.weekday() != 6: raise ValueError("Error: For 'week' type, start date must be Monday and end date must be Sunday.") elif time_type == "month": if start_dt.day != 1 or (end_dt + datetime.timedelta(days=1)).day != 1: raise ValueError( "Error: For 'month' type, start date must be the first day of the month and end date must be the last day of the month.") elif time_type in ["recent7", "recent30"]: if start_date or end_date: raise ValueError( "Error: Start date and end date should not be provided for last_7_days and last_30_days types.") start_dt = end_day - datetime.timedelta(days=6 if time_type == "recent7" else 29) end_dt = end_day else: raise ValueError("Error: Invalid type. Allowed values are: day, week, month, recent7, recent30.") result = [] if time_type == "day": curr_date = start_dt while curr_date <= end_dt: date_str = curr_date.strftime(date_format) result.append(f"{date_str}|{date_str}") curr_date += datetime.timedelta(days=1) elif time_type == "week": curr_date = start_dt while curr_date <= end_dt: start_of_week = curr_date - datetime.timedelta(days=curr_date.weekday()) end_of_week = start_of_week + datetime.timedelta(days=6) if end_of_week > end_dt: end_of_week = end_dt result.append(f"{start_of_week.strftime(date_format)}|{end_of_week.strftime(date_format)}") curr_date = end_of_week + datetime.timedelta(days=1) elif time_type == "month": curr_date = start_dt while curr_date <= end_dt: start_of_month = curr_date.replace(day=1) next_month_start = (start_of_month + datetime.timedelta(days=32)).replace(day=1) end_of_month = next_month_start - datetime.timedelta(days=1) if end_of_month > end_dt: end_of_month = end_dt result.append(f"{start_of_month.strftime(date_format)}|{end_of_month.strftime(date_format)}") curr_date = next_month_start elif time_type == "recent7" or time_type == "recent30": curr_date = start_dt while curr_date <= end_dt: date_str = curr_date.strftime(date_format) result.append(f"{date_str}|{date_str}") curr_date += datetime.timedelta(days=1) return result @staticmethod def init_mapping_flows(flows_path): """ 初始化映射表 """ df = pd.read_excel(flows_path, header=[0, 1, 2]) # 填充空值 df.fillna(method='ffill', inplace=True) new_flows = df['新旧版流量对照']['新版'] old_flows = df['新旧版流量对照']['旧版'] mapping_flows = [] for x, y in zip(new_flows.values, old_flows.values): # 映射数据 new_flows = {"level_one_flows": x[0], "level_two_flows": x[1], "level_three_flows": x[2], "leaf_flows": x[3]} old_flows = {"level_one_flows": y[0], "level_two_flows": y[1], "level_three_flows": y[2], "leaf_flows": y[3]} # 添加到映射表 mapping_flows.append({"new_flows": new_flows, "old_flows": old_flows}) return mapping_flows @staticmethod def seach_mapping_flows(mapping_flows, res_return=False, leaf_flows_name="", mapping_new_or_old="new_flows"): """ 搜索映射表 :param flows_level: 流量等级 :param flows_name: 流量名称 :param mapping_new_or_old: 映射新旧版 """ mapping_new_or_old_all = {"new_flows": "old_flows", "old_flows": "new_flows"} if mapping_new_or_old not in mapping_new_or_old_all: raise ValueError("mapping_new_or_old must be in mapping_new_or_old_all 'new_flows' or 'old_flows'") return_flows = mapping_new_or_old if res_return: return_flows = mapping_new_or_old_all[mapping_new_or_old] result = [] for mapping in mapping_flows: if mapping_new_or_old == "new_flows": if mapping["new_flows"]["leaf_flows"] == leaf_flows_name: result.append(mapping[return_flows]) elif mapping_new_or_old == "old_flows": if mapping["old_flows"]["leaf_flows"] == leaf_flows_name: result.append(mapping[return_flows]) # 去重 seen = set() unique_list = [] for d in result: # frozenset将字典条目转换为不可变集合 tuple_d = frozenset(d.items()) if tuple_d not in seen: seen.add(tuple_d) unique_list.append(d) return unique_list @staticmethod def day_yesterday(): # 获取当前日期和时间 now = datetime.datetime.now() # 计算前天的日期 day_yesterday = now - datetime.timedelta(days=1) yesterday = day_yesterday.strftime("%Y-%m-%d") return yesterday def max_day_check(self, input_date_str): input_date = datetime.datetime.strptime(input_date_str, "%Y-%m-%d") # 获取昨天的日期 yesterday = datetime.datetime.now() - datetime.timedelta(days=1) # 比较输入日期和昨天的日期 if input_date > yesterday: return self.__gr.DateTime(include_time=False, min_width=10, type='string', value=self.day_yesterday()) else: return self.__gr.DateTime(include_time=False, min_width=10, type='string', value=input_date_str)
08-15
# -*- coding: utf-8 -*- import os import cx_Oracle as cx import pandas as pd import datetime as dt import schedule import time import requests from threading import Thread import base64 import hashlib import matplotlib.pyplot as plt import numpy as np from matplotlib.patches import Rectangle class Wechat: def __init__(self, secret): self.secret = secret def access_token(self): """获取企业微信access_token""" url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=wwed5e7f3fd1a3a553&corpsecret={self.secret}' response = requests.get(url).json() return response['access_token'] def oracle_connect(sql): """Oracle数据库连接查询""" con = cx.connect("admin/xxb20140415@192.168.3.16:1521/zzjdata") cursor = con.cursor() cursor.execute(sql) column_s = [col[0] for col in cursor.description] data = cursor.fetchall() cursor.close() con.close() return data, column_s def generate_store_performance_sql(): """生成门店绩效环比数据SQL""" sql = """ select a.c_mdfq 大区,a.c_dq 地市,a.c_mdfq1 地区,a.tjbh 门店编码,a.mc 门店名称,b.lb 类别,nvl(b.jshj,0) 本周销售,nvl(b.ml,0) 本周毛利,nvl(c.jshj,0) 上周销售,nvl(c.ml,0) 上周毛利 from (select a.subbh,nvl(e.sx,'未分类') lb,sum(a.jshj) jshj,sum(a.zhml) ml from c_zkmtmdspxshzb a left join (select * from spjxlbz where lbz='1') e on e.hh=a.hh where a.kdrq BETWEEN TRUNC(SYSDATE, 'IW') AND TRUNC(SYSDATE-1) group by a.subbh,nvl(e.sx,'未分类')) b left join (select a.subbh,nvl(e.sx,'未分类') lb,sum(a.jshj) jshj,sum(a.zhml) ml from c_zkmtmdspxshzb a left join (select * from spjxlbz where lbz='1') e on e.hh=a.hh where a.kdrq BETWEEN TRUNC(SYSDATE, 'IW') - 7 AND TRUNC(SYSDATE- 1) - 7 group by a.subbh,nvl(e.sx,'未分类')) c on b.subbh=c.subbh and b.lb=c.lb left join gl_custom a on a.tjbh=b.subbh and a.tjbh=c.subbh where a.c_mdfq <>' 'and a.c_dq<>' ' and a.c_mdfq1<>' ' and a.c_mdfq <>'电商' and a.c_mdfq <>'大客户部' and a.c_mdfq <>'海南地区' and a.c_mdfq <>'黑龙江地区' """ return sql def calculate_growth_rate(current_value, last_value): """计算环比增长率""" if last_value == 0: return 0 return round((current_value - last_value) / last_value * 100, 2) def calculate_profit_margin(sales, profit): """计算毛利率""" if sales == 0: return 0 return round(profit / sales * 100, 2) def transform_data_for_region_comparison(df): """转换数据为大区环比对比表格式(转置后)- 合并类别项""" try: print(f"原始数据行数: {len(df)}") print(f"大区列表: {sorted(df['大区'].unique())}") print(f"类别列表: {sorted(df['类别'].unique())}") # 获取所有类别 categories = sorted(df['类别'].unique()) regions = sorted(df['大区'].unique()) print(f"处理类别: {categories}") print(f"处理大区: {regions}") # 准备转置后的数据结构 result_data = [] # 添加总计行数据 total_current_sales = df['本周销售'].sum() total_last_sales = df['上周销售'].sum() total_current_profit = df['本周毛利'].sum() total_last_profit = df['上周毛利'].sum() # 计算全国数据 total_sales_growth = calculate_growth_rate(total_current_sales, total_last_sales) total_profit_growth = calculate_growth_rate(total_current_profit, total_last_profit) total_current_margin = calculate_profit_margin(total_current_sales, total_current_profit) total_last_margin = calculate_profit_margin(total_last_sales, total_last_profit) total_margin_growth = calculate_growth_rate(total_current_margin, total_last_margin) # 为每个类别添加三行数据(销售环比、毛利环比、毛利率环比) for category in categories: # 获取该类别数据 cat_df = df[df['类别'] == category] # 计算全国该类别数据 cat_current_sales = cat_df['本周销售'].sum() cat_last_sales = cat_df['上周销售'].sum() cat_current_profit = cat_df['本周毛利'].sum() cat_last_profit = cat_df['本周毛利'].sum() # 销售环比行 - 合并类别,同一类别只显示一次 sales_row = {'类别': category, '指标': '销售环比', 'row_type': 'category_start'} # 毛利环比行 - 类别留空 profit_row = {'类别': '', '指标': '毛利环比', 'row_type': 'category_middle'} # 毛利率环比行 - 类别留空 margin_row = {'类别': '', '指标': '毛利率环比', 'row_type': 'category_end'} # 为每个大区计算数据 for region in regions: region_cat_df = cat_df[cat_df['大区'] == region] # 当前大区该类别数据 region_current_sales = region_cat_df['本周销售'].sum() region_last_sales = region_cat_df['上周销售'].sum() region_current_profit = region_cat_df['本周毛利'].sum() region_last_profit = region_cat_df['上周毛利'].sum() region_current_margin = calculate_profit_margin(region_current_sales, region_current_profit) region_last_margin = calculate_profit_margin(region_last_sales, region_last_profit) # 计算环比 sales_growth = calculate_growth_rate(region_current_sales, region_last_sales) profit_growth = calculate_growth_rate(region_current_profit, region_last_profit) margin_growth = calculate_growth_rate(region_current_margin, region_last_margin) # 填充数据 sales_row[region] = f"{sales_growth:+.2f}%" profit_row[region] = f"{profit_growth:+.2f}%" margin_row[region] = f"{margin_growth:+.2f}%" # 添加全国数据 cat_sales_growth = calculate_growth_rate(cat_current_sales, cat_last_sales) cat_profit_growth = calculate_growth_rate(cat_current_profit, cat_last_profit) cat_current_margin = calculate_profit_margin(cat_current_sales, cat_current_profit) cat_last_margin = calculate_profit_margin(cat_last_sales, cat_last_profit) cat_margin_growth = calculate_growth_rate(cat_current_margin, cat_last_margin) sales_row['全国'] = f"{cat_sales_growth:+.2f}%" profit_row['全国'] = f"{cat_profit_growth:+.2f}%" margin_row['全国'] = f"{cat_margin_growth:+.2f}%" result_data.extend([sales_row, profit_row, margin_row]) # 添加总计行 - 总计行也合并显示 total_sales_row = {'类别': '总计', '指标': '销售环比', 'row_type': 'total_start'} total_profit_row = {'类别': '', '指标': '毛利环比', 'row_type': 'total_middle'} total_margin_row = {'类别': '', '指标': '毛利率环比', 'row_type': 'total_end'} for region in regions: region_df = df[df['大区'] == region] region_current_sales = region_df['本周销售'].sum() region_last_sales = region_df['上周销售'].sum() region_current_profit = region_df['本周毛利'].sum() region_last_profit = region_df['上周毛利'].sum() region_current_margin = calculate_profit_margin(region_current_sales, region_current_profit) region_last_margin = calculate_profit_margin(region_last_sales, region_last_profit) sales_growth = calculate_growth_rate(region_current_sales, region_last_sales) profit_growth = calculate_growth_rate(region_current_profit, region_last_profit) margin_growth = calculate_growth_rate(region_current_margin, region_last_margin) total_sales_row[region] = f"{sales_growth:+.2f}%" total_profit_row[region] = f"{profit_growth:+.2f}%" total_margin_row[region] = f"{margin_growth:+.2f}%" total_sales_row['全国'] = f"{total_sales_growth:+.2f}%" total_profit_row['全国'] = f"{total_profit_growth:+.2f}%" total_margin_row['全国'] = f"{total_margin_growth:+.2f}%" result_data.extend([total_sales_row, total_profit_row, total_margin_row]) # 构建DataFrame columns = ['类别', '指标'] + regions + ['全国'] result_df = pd.DataFrame(result_data)[columns] print(f"大区对比表形状: {result_df.shape}") return result_df, categories, regions, result_data except Exception as e: print(f"大区数据转换失败: {str(e)}") import traceback traceback.print_exc() return None, None, None, None def transform_data_for_city_comparison(df, target_region): """转换数据为指定大区的地市环比对比表格式(转置后)- 合并类别项""" try: region_df = df[df['大区'] == target_region] print(f"处理大区 {target_region}, 数据行数: {len(region_df)}") categories = sorted(region_df['类别'].unique()) cities = sorted(region_df['地市'].unique()) print(f"地市列表: {cities}") print(f"类别列表: {categories}") # 准备转置后的数据结构 result_data = [] # 计算大区合计数据 region_current_sales = region_df['本周销售'].sum() region_last_sales = region_df['上周销售'].sum() region_current_profit = region_df['本周毛利'].sum() region_last_profit = region_df['上周毛利'].sum() region_current_margin = calculate_profit_margin(region_current_sales, region_current_profit) region_last_margin = calculate_profit_margin(region_last_sales, region_last_profit) region_sales_growth = calculate_growth_rate(region_current_sales, region_last_sales) region_profit_growth = calculate_growth_rate(region_current_profit, region_last_profit) region_margin_growth = calculate_growth_rate(region_current_margin, region_last_margin) # 为每个类别添加三行数据 for category in categories: # 获取该类别数据 cat_df = region_df[region_df['类别'] == category] # 计算大区该类别数据 cat_current_sales = cat_df['本周销售'].sum() cat_last_sales = cat_df['上周销售'].sum() cat_current_profit = cat_df['本周毛利'].sum() cat_last_profit = cat_df['本周毛利'].sum() # 销售环比行 - 合并类别,同一类别只显示一次 sales_row = {'类别': category, '指标': '销售环比', 'row_type': 'category_start'} # 毛利环比行 - 类别留空 profit_row = {'类别': '', '指标': '毛利环比', 'row_type': 'category_middle'} # 毛利率环比行 - 类别留空 margin_row = {'类别': '', '指标': '毛利率环比', 'row_type': 'category_end'} # 为每个地市计算数据 for city in cities: city_cat_df = cat_df[cat_df['地市'] == city] # 当前地市该类别数据 city_current_sales = city_cat_df['本周销售'].sum() city_last_sales = city_cat_df['上周销售'].sum() city_current_profit = city_cat_df['本周毛利'].sum() city_last_profit = city_cat_df['上周毛利'].sum() city_current_margin = calculate_profit_margin(city_current_sales, city_current_profit) city_last_margin = calculate_profit_margin(city_last_sales, city_last_profit) # 计算环比 sales_growth = calculate_growth_rate(city_current_sales, city_last_sales) profit_growth = calculate_growth_rate(city_current_profit, city_last_profit) margin_growth = calculate_growth_rate(city_current_margin, city_last_margin) # 填充数据 sales_row[city] = f"{sales_growth:+.2f}%" profit_row[city] = f"{profit_growth:+.2f}%" margin_row[city] = f"{margin_growth:+.2f}%" # 添加大区合计数据 cat_sales_growth = calculate_growth_rate(cat_current_sales, cat_last_sales) cat_profit_growth = calculate_growth_rate(cat_current_profit, cat_last_profit) cat_current_margin = calculate_profit_margin(cat_current_sales, cat_current_profit) cat_last_margin = calculate_profit_margin(cat_last_sales, cat_last_profit) cat_margin_growth = calculate_growth_rate(cat_current_margin, cat_last_margin) sales_row[f'{target_region}合计'] = f"{cat_sales_growth:+.2f}%" profit_row[f'{target_region}合计'] = f"{cat_profit_growth:+.2f}%" margin_row[f'{target_region}合计'] = f"{cat_margin_growth:+.2f}%" result_data.extend([sales_row, profit_row, margin_row]) # 添加总计行 - 总计行也合并显示 total_sales_row = {'类别': '总计', '指标': '销售环比', 'row_type': 'total_start'} total_profit_row = {'类别': '', '指标': '毛利环比', 'row_type': 'total_middle'} total_margin_row = {'类别': '', '指标': '毛利率环比', 'row_type': 'total_end'} for city in cities: city_df = region_df[region_df['地市'] == city] city_current_sales = city_df['本周销售'].sum() city_last_sales = city_df['上周销售'].sum() city_current_profit = city_df['本周毛利'].sum() city_last_profit = city_df['上周毛利'].sum() city_current_margin = calculate_profit_margin(city_current_sales, city_current_profit) city_last_margin = calculate_profit_margin(city_last_sales, city_last_profit) sales_growth = calculate_growth_rate(city_current_sales, city_last_sales) profit_growth = calculate_growth_rate(city_current_profit, city_last_profit) margin_growth = calculate_growth_rate(city_current_margin, city_last_margin) total_sales_row[city] = f"{sales_growth:+.2f}%" total_profit_row[city] = f"{profit_growth:+.2f}%" total_margin_row[city] = f"{margin_growth:+.2f}%" total_sales_row[f'{target_region}合计'] = f"{region_sales_growth:+.2f}%" total_profit_row[f'{target_region}合计'] = f"{region_profit_growth:+.2f}%" total_margin_row[f'{target_region}合计'] = f"{region_margin_growth:+.2f}%" result_data.extend([total_sales_row, total_profit_row, total_margin_row]) # 构建DataFrame columns = ['类别', '指标'] + cities + [f'{target_region}合计'] result_df = pd.DataFrame(result_data)[columns] print(f"地市对比表形状: {result_df.shape}") return result_df, categories, cities, result_data except Exception as e: print(f"地市数据转换失败: {str(e)}") import traceback traceback.print_exc() return None, None, None, None def create_table_image(df, title, output_path, categories, regions, raw_data=None): """使用matplotlib创建转置表格图片 - 支持合并类别居中显示""" try: # 设置中文字体 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False # 准备表格数据 table_data = [] # 构建表头 header = ['类别', '指标'] + list(regions) # 填充数据 for idx, row in df.iterrows(): row_data = [row['类别'], row['指标']] for region in regions: row_data.append(row.get(region, '-')) table_data.append(row_data) # 创建图形 n_rows = len(table_data) + 1 # 数据行数 + 表头行 n_cols = len(header) fig_height = max(8, n_rows * 0.5) # 增加行高以容纳合并单元格 fig_width = max(12, n_cols * 1.8) fig, ax = plt.subplots(figsize=(fig_width, fig_height)) ax.axis('tight') ax.axis('off') # 创建表格 table = ax.table(cellText=table_data, colLabels=header, cellLoc='center', loc='center', bbox=[0, 0, 1, 1]) # 设置表格样式 table.auto_set_font_size(False) table.set_fontsize(8) table.scale(1, 2.0) # 增加行高 # 设置表头样式 for col in range(n_cols): table[(0, col)].set_facecolor('#F4B084') table[(0, col)].set_text_props(weight='bold') # 处理合并单元格和样式 if raw_data: current_category_start = None current_category_rows = 0 total_start = None total_rows = 0 # 第一遍:识别合并区域 for row_idx in range(1, n_rows): row_data_idx = row_idx - 1 if row_data_idx < len(raw_data): row_type = raw_data[row_data_idx].get('row_type', '') if row_type == 'category_start': current_category_start = row_idx current_category_rows = 1 elif row_type == 'category_middle' and current_category_start is not None: current_category_rows += 1 elif row_type == 'category_end' and current_category_start is not None: current_category_rows += 1 # 合并类别单元格 if current_category_rows >= 2: for i in range(current_category_rows): table[(current_category_start + i, 0)].set_facecolor('#F0F0F0') table[(current_category_start + i, 0)].set_text_props(alpha=0) # 隐藏文本 # 在合并区域的中间位置显示类别名称 middle_row = current_category_start + (current_category_rows - 1) // 2 table[(middle_row, 0)].set_text_props(alpha=1, weight='bold') # 显示文本 table[(middle_row, 0)].set_facecolor('#E6F3FF') # 浅蓝色背景突出显示 current_category_start = None current_category_rows = 0 elif row_type == 'total_start': total_start = row_idx total_rows = 1 elif row_type == 'total_middle' and total_start is not None: total_rows += 1 elif row_type == 'total_end' and total_start is not None: total_rows += 1 # 合并总计单元格 if total_rows >= 2: for i in range(total_rows): table[(total_start + i, 0)].set_facecolor('#FFFF00') table[(total_start + i, 0)].set_text_props(alpha=0) # 隐藏文本 # 在合并区域的中间位置显示"总计" middle_row = total_start + (total_rows - 1) // 2 table[(middle_row, 0)].set_text_props(alpha=1, weight='bold', color='#FF0000') total_start = None total_rows = 0 # 设置环比前三名样式(红色字体) for region in regions: # 收集该区域所有类别的销售环比数据 sales_growth_data = [] region_col = header.index(region) for row_idx, row in enumerate(table_data): if row[1] == '销售环比' and row[0] != '总计' and row[0] != '': # 销售环比行且不是总计行和空类别行 value_str = row[region_col] try: growth_value = float(value_str.replace('%', '').replace('+', '')) sales_growth_data.append((row_idx, growth_value)) except: continue # 取前三名 if sales_growth_data: sales_growth_data.sort(key=lambda x: x[1], reverse=True) top3_indices = [idx for idx, _ in sales_growth_data[:3]] # 设置红色字体 for idx in top3_indices: table_row_idx = idx + 1 # +1是因为表头 table[(table_row_idx, region_col)].set_text_props(color='#FF0000', weight='bold') # 设置标题 plt.title(title, fontsize=14, fontweight='bold', pad=20) # 调整布局并保存 plt.tight_layout() plt.savefig(output_path, dpi=300, bbox_inches='tight') plt.close() print(f"✅ 图片生成成功: {output_path}") return True except Exception as e: print(f"❌ 生成图片失败: {str(e)}") import traceback traceback.print_exc() return False def create_performance_report(path, df, categories, regions, report_type, region_name=None, raw_data=None): """创建绩效环比报表图片""" try: # 获取当前日期 now = dt.datetime.now() date_str = now.strftime("%Y%m%d") # 计算上周时间范围 yesterday = now - dt.timedelta(days=1) last_week_start = (yesterday - dt.timedelta(days=yesterday.weekday() + 7)).strftime("%m月%d日") last_week_end = (yesterday - dt.timedelta(days=yesterday.weekday() + 1)).strftime("%m月%d日") this_week_start = (yesterday - dt.timedelta(days=yesterday.weekday())).strftime("%m月%d日") this_week_end = yesterday.strftime("%m月%d日") if report_type == 'region': title = f"各大区各绩效类别环比对比表\n({last_week_start}-{last_week_end} vs {this_week_start}-{this_week_end})" file_prefix = '大区绩效环比表' else: title = f"{region_name}各地市环比对比表\n({last_week_start}-{last_week_end} vs {this_week_start}-{this_week_end})" file_prefix = f'{region_name}地市绩效环比表' # 生成图片 file_mc = f'{file_prefix}_{date_str}' output_path = os.path.join(path, f'{file_mc}.png') success = create_table_image(df, title, output_path, categories, regions, raw_data) if success: print(f"图片生成成功: {output_path}") return output_path else: return None except Exception as e: print(f"生成绩效环比报表失败: {str(e)}") return None def application_push_image(access_token, image_path, user_id="037565", agent_id="1000077"): """推送图片到企业微信(个人)""" try: # 上传图片获取media_id with open(image_path, 'rb') as f: files = {'media': f} upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image' upload_response = requests.post(upload_url, files=files).json() if 'media_id' not in upload_response: print(f"图片上传失败: {upload_response}") return None media_id = upload_response['media_id'] # 发送图片消息 send_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' payload = { "touser": user_id, "msgtype": "image", "agentid": agent_id, "image": {"media_id": media_id}, "safe": 0 } response = requests.post(send_url, json=payload).json() return response except Exception as e: print(f"图片发送失败: {str(e)}") return None def send_store_performance_report(path): """发送门店绩效环比报表""" try: # 初始化微信 wechat = Wechat('tXNMrgcTgeV3IAqJhWB7mOe_bcKe9EtdCDze_75mGeY') access_token = wechat.access_token() # 获取原始数据 sql = generate_store_performance_sql() data, columns = oracle_connect(sql) # 转换为DataFrame df = pd.DataFrame(data, columns=columns) print(f"获取到 {len(df)} 条原始数据") # 数据验证 print("\n数据验证:") print(f"大区列表: {sorted(df['大区'].unique())}") print(f"地市列表: {sorted(df['地市'].unique())}") print(f"类别列表: {sorted(df['类别'].unique())}") image_paths = [] # 1. 生成各大区环比对比表(第一张图片) print("\n" + "=" * 50) print("生成各大区环比对比表") print("=" * 50) region_df, region_categories, region_list, region_raw_data = transform_data_for_region_comparison(df) if region_df is not None and len(region_df) > 0: region_image_path = create_performance_report(path, region_df, region_categories, region_list, 'region', raw_data=region_raw_data) if region_image_path: image_paths.append(('各大区环比对比表', region_image_path)) # 2. 生成各大地市环比对比表(第2-5张图片) regions = sorted(df['大区'].unique()) print(f"\n需要处理的大区: {regions}") for region in regions: print("\n" + "=" * 50) print(f"生成 {region} 地市环比对比表") print("=" * 50) city_df, city_categories, city_list, city_raw_data = transform_data_for_city_comparison(df, region) if city_df is not None and len(city_df) > 0: # 将城市列表转换为显示用的区域列表 display_regions = city_list + [f'{region}合计'] city_image_path = create_performance_report(path, city_df, city_categories, display_regions, 'city', region, raw_data=city_raw_data) if city_image_path: image_paths.append((f'{region}地市环比对比表', city_image_path)) # 推送所有报表图片给个人用户037565 for image_name, image_path in image_paths: if os.path.exists(image_path): # 只发送给个人用户037565 result = application_push_image(access_token, image_path, "037565") if result and result.get('errcode') == 0: print(f"{dt.datetime.now()} - {image_name} 个人推送成功") else: print(f"{dt.datetime.now()} - {image_name} 个人推送失败") # 删除临时文件 os.remove(image_path) return True except Exception as e: print(f"{dt.datetime.now()} - 门店绩效环比报表推送失败: {str(e)}") return False def run_store_performance_scheduler(path): """运行门店绩效环比报表定时任务""" # 每天早上7:30执行 schedule.every().day.at("07:30").do(send_store_performance_report, path=path) # 立即测试一次 print(f"{dt.datetime.now()} - 正在执行门店绩效环比报表首次测试...") send_store_performance_report(path) while True: schedule.run_pending() time.sleep(60) if __name__ == '__main__': print(f'门店绩效环比报表程序启动时间: {dt.datetime.now()}') output_path = r'D:\门店绩效环比报表' if not os.path.exists(output_path): os.makedirs(output_path) # 启动定时任务线程 scheduler_thread = Thread(target=run_store_performance_scheduler, args=(output_path,)) scheduler_thread.daemon = True scheduler_thread.start() try: while True: time.sleep(60) except KeyboardInterrupt: print("门店绩效环比报表程序已停止") 修正代码,把类别同项合并居中处理,环比增长率改为负数加负号,正数不加正号的形式,将各类别各指标销售环比增长率最低的数据用红色字体,将各大区各绩效类别环比对比表最右侧加入合计
10-13
# -*- coding: utf-8 -*- import os import cx_Oracle as cx import pandas as pd import datetime as dt import schedule import time import requests from threading import Thread import base64 import hashlib import matplotlib.pyplot as plt import numpy as np from matplotlib.patches import Rectangle class Wechat: def __init__(self, secret): self.secret = secret def access_token(self): """获取企业微信access_token""" url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=wwed5e7f3fd1a3a553&corpsecret={self.secret}' response = requests.get(url).json() return response['access_token'] def oracle_connect(sql): """Oracle数据库连接查询""" con = cx.connect("admin/xxb20140415@192.168.3.16:1521/zzjdata") cursor = con.cursor() cursor.execute(sql) column_s = [col[0] for col in cursor.description] data = cursor.fetchall() cursor.close() con.close() return data, column_s def generate_store_performance_sql(): """生成门店绩效环比数据SQL""" sql = """ select a.c_mdfq 大区,a.c_dq 地市,a.c_mdfq1 地区,a.tjbh 门店编码,a.mc 门店名称,b.lb 类别,nvl(b.jshj,0) 本周销售,nvl(b.ml,0) 本周毛利,nvl(c.jshj,0) 上周销售,nvl(c.ml,0) 上周毛利 from (select a.subbh,nvl(e.sx,'未分类') lb,sum(a.jshj) jshj,sum(a.zhml) ml from c_zkmtmdspxshzb a left join (select * from spjxlbz where lbz='1') e on e.hh=a.hh where a.kdrq BETWEEN TRUNC(SYSDATE, 'IW') AND TRUNC(SYSDATE-1) group by a.subbh,nvl(e.sx,'未分类')) b left join (select a.subbh,nvl(e.sx,'未分类') lb,sum(a.jshj) jshj,sum(a.zhml) ml from c_zkmtmdspxshzb a left join (select * from spjxlbz where lbz='1') e on e.hh=a.hh where a.kdrq BETWEEN TRUNC(SYSDATE, 'IW') - 7 AND TRUNC(SYSDATE- 1) - 7 group by a.subbh,nvl(e.sx,'未分类')) c on b.subbh=c.subbh and b.lb=c.lb left join gl_custom a on a.tjbh=b.subbh and a.tjbh=c.subbh where a.c_mdfq <>' 'and a.c_dq<>' ' and a.c_mdfq1<>' ' and a.c_mdfq <>'电商' and a.c_mdfq <>'大客户部' and a.c_mdfq <>'海南地区' and a.c_mdfq <>'黑龙江地区' """ return sql def calculate_growth_rate(current_value, last_value): """计算环比增长率""" if last_value == 0: return 0 return round((current_value - last_value) / last_value * 100, 2) def calculate_profit_margin(sales, profit): """计算毛利率""" if sales == 0: return 0 return round(profit / sales * 100, 2) def format_growth_rate(growth_rate): """格式化环比增长率显示:负数加负号,正数不加正号""" if growth_rate > 0: return f"{growth_rate:.2f}%" elif growth_rate < 0: return f"{growth_rate:.2f}%" else: return "0.00%" def transform_data_for_region_comparison(df): """转换数据为大区环比对比表格式(转置后)- 合并类别项,并添加合计列""" try: print(f"原始数据行数: {len(df)}") print(f"大区列表: {sorted(df['大区'].unique())}") print(f"类别列表: {sorted(df['类别'].unique())}") categories = sorted(df['类别'].unique()) regions = sorted(df['大区'].unique()) result_data = [] # 全国总计 total_current_sales = df['本周销售'].sum() total_last_sales = df['上周销售'].sum() total_current_profit = df['本周毛利'].sum() total_last_profit = df['上周毛利'].sum() total_sales_growth = calculate_growth_rate(total_current_sales, total_last_sales) total_profit_growth = calculate_growth_rate(total_current_profit, total_last_profit) total_current_margin = calculate_profit_margin(total_current_sales, total_current_profit) total_last_margin = calculate_profit_margin(total_last_sales, total_last_profit) total_margin_growth = calculate_growth_rate(total_current_margin, total_last_margin) # 为每个类别处理 for category in categories: cat_df = df[df['类别'] == category] # 类别内全国数据 cat_current_sales = cat_df['本周销售'].sum() cat_last_sales = cat_df['上周销售'].sum() cat_current_profit = cat_df['本周毛利'].sum() cat_last_profit = cat_df['上周毛利'].sum() cat_sales_growth = calculate_growth_rate(cat_current_sales, cat_last_sales) cat_profit_growth = calculate_growth_rate(cat_current_profit, cat_last_profit) cat_current_margin = calculate_profit_margin(cat_current_sales, cat_current_profit) cat_last_margin = calculate_profit_margin(cat_last_sales, cat_last_profit) cat_margin_growth = calculate_growth_rate(cat_current_margin, cat_last_margin) # 销售、毛利、毛利率三行(用于表格) sales_row = {'类别': category, '指标': '销售环比', 'row_type': 'category_start'} profit_row = {'类别': '', '指标': '毛利环比', 'row_type': 'category_middle'} margin_row = {'类别': '', '指标': '毛利率环比', 'row_type': 'category_end'} # 计算每个大区 + 合计 for region in regions: region_cat_df = cat_df[cat_df['大区'] == region] rcs = region_cat_df['本周销售'].sum() rls = region_cat_df['上周销售'].sum() rcp = region_cat_df['本周毛利'].sum() rlp = region_cat_df['上周毛利'].sum() rcm = calculate_profit_margin(rcs, rcp) rlm = calculate_profit_margin(rls, rlp) sg = calculate_growth_rate(rcs, rls) pg = calculate_growth_rate(rcp, rlp) mg = calculate_growth_rate(rcm, rlm) sales_row[region] = format_growth_rate(sg) profit_row[region] = format_growth_rate(pg) margin_row[region] = format_growth_rate(mg) # 🔽 新增:该类别所有大区平均/加权?这里采用销售额加权平均 # 简单做法:取所有门店汇总后的整体增长率(已在上面计算过 cat_sales_growth) sales_row['合计'] = format_growth_rate(cat_sales_growth) profit_row['合计'] = format_growth_rate(cat_profit_growth) margin_row['合计'] = format_growth_rate(cat_margin_growth) result_data.extend([sales_row, profit_row, margin_row]) # 🔽 总计行(所有类别汇总) total_sales_row = {'类别': '总计', '指标': '销售环比', 'row_type': 'total_start'} total_profit_row = {'类别': '', '指标': '毛利环比', 'row_type': 'total_middle'} total_margin_row = {'类别': '', '指标': '毛利率环比', 'row_type': 'total_end'} for region in regions: region_df = df[df['大区'] == region] rcs = region_df['本周销售'].sum() rls = region_df['上周销售'].sum() rcp = region_df['本周毛利'].sum() rlp = region_df['上周毛利'].sum() rcm = calculate_profit_margin(rcs, rcp) rlm = calculate_profit_margin(rls, rlp) sg = calculate_growth_rate(rcs, rls) pg = calculate_growth_rate(rcp, rlp) mg = calculate_growth_rate(rcm, rlm) total_sales_row[region] = format_growth_rate(sg) total_profit_row[region] = format_growth_rate(pg) total_margin_row[region] = format_growth_rate(mg) # 总计行的合计列 total_sales_row['合计'] = format_growth_rate(total_sales_growth) total_profit_row['合计'] = format_growth_rate(total_profit_growth) total_margin_row['合计'] = format_growth_rate(total_margin_growth) result_data.extend([total_sales_row, total_profit_row, total_margin_row]) columns = ['类别', '指标'] + regions + ['合计'] result_df = pd.DataFrame(result_data)[columns] print(f"大区对比表形状: {result_df.shape}") return result_df, categories, regions, result_data except Exception as e: print(f"大区数据转换失败: {str(e)}") import traceback traceback.print_exc() return None, None, None, None def transform_data_for_city_comparison(df, target_region): """转换数据为指定大区的地市环比对比表格式(转置后)- 合并类别项""" try: region_df = df[df['大区'] == target_region] print(f"处理大区 {target_region}, 数据行数: {len(region_df)}") categories = sorted(region_df['类别'].unique()) cities = sorted(region_df['地市'].unique()) print(f"地市列表: {cities}") print(f"类别列表: {categories}") # 准备转置后的数据结构 result_data = [] # 计算大区合计数据 region_current_sales = region_df['本周销售'].sum() region_last_sales = region_df['上周销售'].sum() region_current_profit = region_df['本周毛利'].sum() region_last_profit = region_df['上周毛利'].sum() region_current_margin = calculate_profit_margin(region_current_sales, region_current_profit) region_last_margin = calculate_profit_margin(region_last_sales, region_last_profit) region_sales_growth = calculate_growth_rate(region_current_sales, region_last_sales) region_profit_growth = calculate_growth_rate(region_current_profit, region_last_profit) region_margin_growth = calculate_growth_rate(region_current_margin, region_last_margin) # 为每个类别添加三行数据 for category in categories: # 获取该类别数据 cat_df = region_df[region_df['类别'] == category] # 计算大区该类别数据 cat_current_sales = cat_df['本周销售'].sum() cat_last_sales = cat_df['上周销售'].sum() cat_current_profit = cat_df['本周毛利'].sum() cat_last_profit = cat_df['本周毛利'].sum() # 销售环比行 - 合并类别,同一类别只显示一次 sales_row = {'类别': category, '指标': '销售环比', 'row_type': 'category_start'} # 毛利环比行 - 类别留空 profit_row = {'类别': '', '指标': '毛利环比', 'row_type': 'category_middle'} # 毛利率环比行 - 类别留空 margin_row = {'类别': '', '指标': '毛利率环比', 'row_type': 'category_end'} # 为每个地市计算数据 for city in cities: city_cat_df = cat_df[cat_df['地市'] == city] # 当前地市该类别数据 city_current_sales = city_cat_df['本周销售'].sum() city_last_sales = city_cat_df['上周销售'].sum() city_current_profit = city_cat_df['本周毛利'].sum() city_last_profit = city_cat_df['上周毛利'].sum() city_current_margin = calculate_profit_margin(city_current_sales, city_current_profit) city_last_margin = calculate_profit_margin(city_last_sales, city_last_profit) # 计算环比 sales_growth = calculate_growth_rate(city_current_sales, city_last_sales) profit_growth = calculate_growth_rate(city_current_profit, city_last_profit) margin_growth = calculate_growth_rate(city_current_margin, city_last_margin) # 填充数据 sales_row[city] = f"{sales_growth:+.2f}%" profit_row[city] = f"{profit_growth:+.2f}%" margin_row[city] = f"{margin_growth:+.2f}%" # 添加大区合计数据 cat_sales_growth = calculate_growth_rate(cat_current_sales, cat_last_sales) cat_profit_growth = calculate_growth_rate(cat_current_profit, cat_last_profit) cat_current_margin = calculate_profit_margin(cat_current_sales, cat_current_profit) cat_last_margin = calculate_profit_margin(cat_last_sales, cat_last_profit) cat_margin_growth = calculate_growth_rate(cat_current_margin, cat_last_margin) sales_row[f'{target_region}合计'] = f"{cat_sales_growth:+.2f}%" profit_row[f'{target_region}合计'] = f"{cat_profit_growth:+.2f}%" margin_row[f'{target_region}合计'] = f"{cat_margin_growth:+.2f}%" result_data.extend([sales_row, profit_row, margin_row]) # 添加总计行 - 总计行也合并显示 total_sales_row = {'类别': '总计', '指标': '销售环比', 'row_type': 'total_start'} total_profit_row = {'类别': '', '指标': '毛利环比', 'row_type': 'total_middle'} total_margin_row = {'类别': '', '指标': '毛利率环比', 'row_type': 'total_end'} for city in cities: city_df = region_df[region_df['地市'] == city] city_current_sales = city_df['本周销售'].sum() city_last_sales = city_df['上周销售'].sum() city_current_profit = city_df['本周毛利'].sum() city_last_profit = city_df['上周毛利'].sum() city_current_margin = calculate_profit_margin(city_current_sales, city_current_profit) city_last_margin = calculate_profit_margin(city_last_sales, city_last_profit) sales_growth = calculate_growth_rate(city_current_sales, city_last_sales) profit_growth = calculate_growth_rate(city_current_profit, city_last_profit) margin_growth = calculate_growth_rate(city_current_margin, city_last_margin) total_sales_row[city] = f"{sales_growth:+.2f}%" total_profit_row[city] = f"{profit_growth:+.2f}%" total_margin_row[city] = f"{margin_growth:+.2f}%" total_sales_row[f'{target_region}合计'] = f"{region_sales_growth:+.2f}%" total_profit_row[f'{target_region}合计'] = f"{region_profit_growth:+.2f}%" total_margin_row[f'{target_region}合计'] = f"{region_margin_growth:+.2f}%" result_data.extend([total_sales_row, total_profit_row, total_margin_row]) # 构建DataFrame columns = ['类别', '指标'] + cities + [f'{target_region}合计'] result_df = pd.DataFrame(result_data)[columns] print(f"地市对比表形状: {result_df.shape}") return result_df, categories, cities, result_data except Exception as e: print(f"地市数据转换失败: {str(e)}") import traceback traceback.print_exc() return None, None, None, None def create_table_image(df, title, output_path, categories, regions, raw_data=None): """使用matplotlib创建转置表格图片,正确实现类别合并与居中""" try: plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False # 构建表头 header = ['类别', '指标'] + list(regions) + ['合计'] # 包含合计列 table_data = [] for _, row in df.iterrows(): row_data = [row['类别'], row['指标']] for col in regions + ['合计']: row_data.append(row.get(col, '-')) table_data.append(row_data) n_rows = len(table_data) + 1 n_cols = len(header) fig_height = max(8, n_rows * 0.6) fig_width = max(14, n_cols * 1.6) fig, ax = plt.subplots(figsize=(fig_width, fig_height)) ax.axis('tight') ax.axis('off') # 创建表格 table = ax.table(cellText=table_data, colLabels=header, cellLoc='center', loc='center', bbox=[0, 0, 1, 1]) table.auto_set_font_size(False) table.set_fontsize(9) table.scale(1, 2.2) # 增加行高,便于居中 # 设置表头样式 for j in range(n_cols): table[(0, j)].set_facecolor('#F4B084') table[(0, j)].set_text_props(weight='bold', color='white') # ----------------------------- # 🔧 实现“类别”列跨三行合并 & 垂直居中 # ----------------------------- if raw_data is not None: i = 1 # 数据行索引(从表格第1行开始) while i < n_rows: row_type = raw_data[i - 1].get('row_type', '') if row_type == 'category_start': # 检查是否有后续两行属于同一类别 if i + 2 < n_rows: # 获取这三行对应的原始数据类型 next1_type = raw_data[i].get('row_type', '') next2_type = raw_data[i + 1].get('row_type', '') if next1_type == 'category_middle' and next2_type == 'category_end': # ✅ 可以合并这三行 # 清除第二、第三行的类别文本 table[(i + 1, 0)].set_text_props(text='') table[(i + 2, 0)].set_text_props(text='') # 设置统一背景色(浅灰) for r in [i, i + 1, i + 2]: table[(r, 0)].set_facecolor('#E6F3FF') # 计算垂直居中位置:第 i+1 行(中间) middle_row_idx = i + 1 category_name = raw_data[i - 1]['类别'] # 在中间行重新设置类别文本(覆盖默认值) cell = table[(middle_row_idx, 0)] cell._text.set_text(category_name) cell._text.set_weight('bold') cell._text.set_color('black') # 可选:添加边框强调 for r in [i, i + 1, i + 2]: table[(r, 0)].set_edgecolor('black') table[(r, 0)].visible_edges = ['left', 'right', 'top', 'bottom'] # 跳过已处理的两行 i += 3 continue elif row_type == 'total_start': # 处理“总计”三行合并 if i + 2 < n_rows: if (raw_data[i].get('row_type') == 'total_middle' and raw_data[i + 1].get('row_type') == 'total_end'): for r in [i, i + 1, i + 2]: table[(r, 0)].set_facecolor('#FFFFCC') table[(r, 0)]._text.set_text('') middle_row_idx = i + 1 cell = table[(middle_row_idx, 0)] cell._text.set_text("总计") cell._text.set_weight('bold') cell._text.set_color('red') cell.set_facecolor('#FFFFCC') i += 3 continue i += 1 # ----------------------------- # 🔴 标红每列中“销售环比”的最低三项 # ----------------------------- sales_rows = [] for idx, row in enumerate(table_data): if row[1] == '销售环比' and row[0] not in ['', '总计']: # 排除空和总计 sales_rows.append(idx) for col_name in header: if col_name in ['类别', '指标']: # 跳过非数值列 continue col_idx = header.index(col_name) values = [] for row_idx in sales_rows: text = table_data[row_idx][col_idx] try: num = float(text.replace('%', '').replace('-', '').replace('+', '')) if text.startswith('-'): num = -num values.append((row_idx, num)) except: continue # 排序取最小3个 sorted_vals = sorted(values, key=lambda x: x[1]) bottom3_row_indices = [x[0] for x in sorted_vals[:3]] for row_idx in bottom3_row_indices: table_cell = table[(row_idx + 1, col_idx)] # +1 因为有表头 table_cell._text.set_color('red') table_cell._text.set_weight('bold') # 设置标题 plt.title(title, fontsize=14, fontweight='bold', pad=40) plt.tight_layout() plt.savefig(output_path, dpi=300, bbox_inches='tight', facecolor='white') plt.close() print(f"✅ 图片生成成功: {output_path}") return True except Exception as e: print(f"❌ 生成图片失败: {str(e)}") import traceback traceback.print_exc() return False def create_performance_report(path, df, categories, regions, report_type, region_name=None, raw_data=None): """创建绩效环比报表图片""" try: # 获取当前日期 now = dt.datetime.now() date_str = now.strftime("%Y%m%d") # 计算上周时间范围 yesterday = now - dt.timedelta(days=1) last_week_start = (yesterday - dt.timedelta(days=yesterday.weekday() + 7)).strftime("%m月%d日") last_week_end = (yesterday - dt.timedelta(days=yesterday.weekday() + 1)).strftime("%m月%d日") this_week_start = (yesterday - dt.timedelta(days=yesterday.weekday())).strftime("%m月%d日") this_week_end = yesterday.strftime("%m月%d日") if report_type == 'region': title = f"各大区各绩效类别环比对比表\n({last_week_start}-{last_week_end} vs {this_week_start}-{this_week_end})" file_prefix = '大区绩效环比表' else: title = f"{region_name}各地市环比对比表\n({last_week_start}-{last_week_end} vs {this_week_start}-{this_week_end})" file_prefix = f'{region_name}地市绩效环比表' # 生成图片 file_mc = f'{file_prefix}_{date_str}' output_path = os.path.join(path, f'{file_mc}.png') success = create_table_image(df, title, output_path, categories, regions, raw_data) if success: print(f"图片生成成功: {output_path}") return output_path else: return None except Exception as e: print(f"生成绩效环比报表失败: {str(e)}") return None def application_push_image(access_token, image_path, user_id="037565", agent_id="1000077"): """推送图片到企业微信(个人)""" try: # 上传图片获取media_id with open(image_path, 'rb') as f: files = {'media': f} upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image' upload_response = requests.post(upload_url, files=files).json() if 'media_id' not in upload_response: print(f"图片上传失败: {upload_response}") return None media_id = upload_response['media_id'] # 发送图片消息 send_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' payload = { "touser": user_id, "msgtype": "image", "agentid": agent_id, "image": {"media_id": media_id}, "safe": 0 } response = requests.post(send_url, json=payload).json() return response except Exception as e: print(f"图片发送失败: {str(e)}") return None def send_store_performance_report(path): """发送门店绩效环比报表""" try: # 初始化微信 wechat = Wechat('tXNMrgcTgeV3IAqJhWB7mOe_bcKe9EtdCDze_75mGeY') access_token = wechat.access_token() # 获取原始数据 sql = generate_store_performance_sql() data, columns = oracle_connect(sql) # 转换为DataFrame df = pd.DataFrame(data, columns=columns) print(f"获取到 {len(df)} 条原始数据") # 数据验证 print("\n数据验证:") print(f"大区列表: {sorted(df['大区'].unique())}") print(f"地市列表: {sorted(df['地市'].unique())}") print(f"类别列表: {sorted(df['类别'].unique())}") image_paths = [] # 1. 生成各大区环比对比表(第一张图片) print("\n" + "=" * 50) print("生成各大区环比对比表") print("=" * 50) region_df, region_categories, region_list, region_raw_data = transform_data_for_region_comparison(df) if region_df is not None and len(region_df) > 0: region_image_path = create_performance_report(path, region_df, region_categories, region_list, 'region', raw_data=region_raw_data) if region_image_path: image_paths.append(('各大区环比对比表', region_image_path)) # 2. 生成各大地市环比对比表(第2-5张图片) regions = sorted(df['大区'].unique()) print(f"\n需要处理的大区: {regions}") for region in regions: print("\n" + "=" * 50) print(f"生成 {region} 地市环比对比表") print("=" * 50) city_df, city_categories, city_list, city_raw_data = transform_data_for_city_comparison(df, region) if city_df is not None and len(city_df) > 0: # 将城市列表转换为显示用的区域列表 display_regions = city_list + [f'{region}合计'] city_image_path = create_performance_report(path, city_df, city_categories, display_regions, 'city', region, raw_data=city_raw_data) if city_image_path: image_paths.append((f'{region}地市环比对比表', city_image_path)) # 推送所有报表图片给个人用户037565 for image_name, image_path in image_paths: if os.path.exists(image_path): # 只发送给个人用户037565 result = application_push_image(access_token, image_path, "037565") if result and result.get('errcode') == 0: print(f"{dt.datetime.now()} - {image_name} 个人推送成功") else: print(f"{dt.datetime.now()} - {image_name} 个人推送失败") # 删除临时文件 os.remove(image_path) return True except Exception as e: print(f"{dt.datetime.now()} - 门店绩效环比报表推送失败: {str(e)}") return False def run_store_performance_scheduler(path): """运行门店绩效环比报表定时任务""" # 每天早上7:30执行 schedule.every().day.at("07:30").do(send_store_performance_report, path=path) # 立即测试一次 print(f"{dt.datetime.now()} - 正在执行门店绩效环比报表首次测试...") send_store_performance_report(path) while True: schedule.run_pending() time.sleep(60) if __name__ == '__main__': print(f'门店绩效环比报表程序启动时间: {dt.datetime.now()}') output_path = r'D:\门店绩效环比报表' if not os.path.exists(output_path): os.makedirs(output_path) # 启动定时任务线程 scheduler_thread = Thread(target=run_store_performance_scheduler, args=(output_path,)) scheduler_thread.daemon = True scheduler_thread.start() try: while True: time.sleep(60) except KeyboardInterrupt: print("门店绩效环比报表程序已停止") E:\Anaconda3-2024.02.1\python.exe E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py 门店绩效环比报表程序启动时间: 2025-10-12 09:43:51.970665 2025-10-12 09:43:51.971662 - 正在执行门店绩效环比报表首次测试... 获取到 24499 条原始数据 数据验证: 大区列表: ['豫北大区', '豫南大区', '郑州东区', '郑州西区'] 地市列表: ['三门峡地区', '信阳地区', '南阳地区', '周口地区', '商丘地区', '安阳地区', '平顶山地区', '开封地区', '新乡地区', '洛阳地区', '济源地区', '漯河地区', '濮阳地区', '焦作地区', '许昌地区', '郑东1区', '郑东2区', '郑东3区', '郑西1区', '郑西2区', '郑西3区', '驻马店地区', '鹤壁地区'] 类别列表: ['A', 'A+', 'B', 'C', 'C+', 'D', 'D+', 'E', 'F', 'b', 'c', '未分类'] ================================================== 生成各大区环比对比表 ================================================== 原始数据行数: 24499 大区列表: ['豫北大区', '豫南大区', '郑州东区', '郑州西区'] 类别列表: ['A', 'A+', 'B', 'C', 'C+', 'D', 'D+', 'E', 'F', 'b', 'c', '未分类'] 大区对比表形状: (39, 7) E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py:338: UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail. fig, ax = plt.subplots(figsize=(fig_width, fig_height)) ❌ 生成图片失败: unhashable type: 'list' Traceback (most recent call last): File "E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py", line 397, in create_table_image table[(r, 0)].visible_edges = ['left', 'right', 'top', 'bottom'] ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "E:\Anaconda3-2024.02.1\Lib\site-packages\matplotlib\table.py", line 209, in visible_edges elif value in self._edge_aliases: ^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: unhashable type: 'list' 需要处理的大区: ['豫北大区', '豫南大区', '郑州东区', '郑州西区'] ================================================== 生成 豫北大区 地市环比对比表 ================================================== 处理大区 豫北大区, 数据行数: 7583 地市列表: ['三门峡地区', '商丘地区', '安阳地区', '开封地区', '新乡地区', '洛阳地区', '济源地区', '濮阳地区', '焦作地区', '鹤壁地区'] 类别列表: ['A', 'A+', 'B', 'C', 'C+', 'D', 'D+', 'E', 'F', 'b', 'c', '未分类'] 地市对比表形状: (39, 13) E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py:338: UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail. fig, ax = plt.subplots(figsize=(fig_width, fig_height)) Traceback (most recent call last): File "E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py", line 397, in create_table_image table[(r, 0)].visible_edges = ['left', 'right', 'top', 'bottom'] ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "E:\Anaconda3-2024.02.1\Lib\site-packages\matplotlib\table.py", line 209, in visible_edges elif value in self._edge_aliases: ^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: unhashable type: 'list' ❌ 生成图片失败: unhashable type: 'list' ================================================== 生成 豫南大区 地市环比对比表 ================================================== 处理大区 豫南大区, 数据行数: 8132 地市列表: ['信阳地区', '南阳地区', '周口地区', '平顶山地区', '漯河地区', '许昌地区', '驻马店地区'] 类别列表: ['A', 'A+', 'B', 'C', 'C+', 'D', 'D+', 'E', 'F', 'b', 'c', '未分类'] 地市对比表形状: (39, 10) E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py:338: UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail. fig, ax = plt.subplots(figsize=(fig_width, fig_height)) ❌ 生成图片失败: unhashable type: 'list' ================================================== 生成 郑州东区 地市环比对比表 ================================================== 处理大区 郑州东区, 数据行数: 4559 地市列表: ['郑东1区', '郑东2区', '郑东3区'] 类别列表: ['A', 'A+', 'B', 'C', 'C+', 'D', 'D+', 'E', 'F', 'b', 'c', '未分类'] Traceback (most recent call last): File "E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py", line 397, in create_table_image table[(r, 0)].visible_edges = ['left', 'right', 'top', 'bottom'] ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "E:\Anaconda3-2024.02.1\Lib\site-packages\matplotlib\table.py", line 209, in visible_edges elif value in self._edge_aliases: ^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: unhashable type: 'list' 地市对比表形状: (39, 6) E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py:338: UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail. fig, ax = plt.subplots(figsize=(fig_width, fig_height)) Traceback (most recent call last): File "E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py", line 397, in create_table_image table[(r, 0)].visible_edges = ['left', 'right', 'top', 'bottom'] ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "E:\Anaconda3-2024.02.1\Lib\site-packages\matplotlib\table.py", line 209, in visible_edges elif value in self._edge_aliases: ^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: unhashable type: 'list' ❌ 生成图片失败: unhashable type: 'list' ================================================== 生成 郑州西区 地市环比对比表 ================================================== 处理大区 郑州西区, 数据行数: 4225 地市列表: ['郑西1区', '郑西2区', '郑西3区'] 类别列表: ['A', 'A+', 'B', 'C', 'C+', 'D', 'D+', 'E', 'F', 'b', 'c', '未分类'] 地市对比表形状: (39, 6) E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py:338: UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail. fig, ax = plt.subplots(figsize=(fig_width, fig_height)) ❌ 生成图片失败: unhashable type: 'list' Traceback (most recent call last): File "E:\Pycharm-2023.3.4\project\pythonProject\venv\推送\1-部长绩效类别周一早推送.py", line 397, in create_table_image table[(r, 0)].visible_edges = ['left', 'right', 'top', 'bottom'] ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "E:\Anaconda3-2024.02.1\Lib\site-packages\matplotlib\table.py", line 209, in visible_edges elif value in self._edge_aliases: ^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: unhashable type: 'list'
10-13
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值