对 HTML 导出的 DataFrame 应用了多个格式规则?

Multiple formatting rules applied on HTML-exported DataFrame?

我需要通过函数将 pandas DataFrame 粘贴到新生成的电子邮件草稿的 HTML 正文中。问题是,在电子邮件草稿结束之前,我需要对其应用多个条件样式。

import datetime as dt
import pandas as pd
import win32com.client as win32

def create_mail(text, subject, recipient):
    outlook = win32.Dispatch('outlook.application')
    mail = outlook.CreateItem(0)
    mail.To = recipient
    mail.Subject = subject
    mail.HtmlBody = text
    mail.save()

df = pd.DataFrame(data = {'Number': [100.00, -100.00], 'Date': [dt.date(2020, 1, 1), dt.date(2022, 1, 1)]})
df['Date'] = pd.to_datetime(df['Date']).dt.date

样式定义如下:

def style_negative(v, props = 'color:red;'):
    return props if v < 0 else None
def style_red_date(v, props = 'color:red;'):
    return props if v < dt.datetime.now().date() else None

Number 列中的负数必须为红色。 Date 列中今天之前的日期也必须标为红色。

如果我通过 df.style.applymap() 只对 DataFrame 对象应用一种(任一)样式,它工作得很好。但是,当我想通过 .apply()

将另一种样式应用于(现在)Styler 对象时
df = df.style.applymap(style_negative, subset = ['Number'])
df = df.apply(style_red_date, subset = ['Date'])

我收到以下错误:

ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

我的其余代码:

html = (
    df
    .format({'Number':"{:.2f}"})
    .set_properties(**{'font-size':'11pt'})
    .hide_index()
    .render()
    )

mail_content = """
<html><head></head><body>
{0}
</body></html>
""".format(html)

create_mail(mail_content, "Subject", "")

我已经通读了 this 关于从 Styler 对象导出样式的问题,但它似乎只能用于 DataFrame 对象。有没有办法将多个格式规则应用于 DataFrame / Styler 对象并将其导出为 HTML?我是不是遗漏了一些微不足道的东西?

我们在这里只需要 np.where 而不是内联 if else

一些其他注意事项:

  1. 创建 Styler object from DataFrame.style we should save that as a variable called styler or something similar instead of df to denote the type of object has changed from DataFrame to Styler
  2. 在向已声明的 Styler 对象添加额外样式时,我们不需要重新分配
  3. 我们需要 np.where 创建一个 ndarray 样式,基于对 Series[= 进行布尔运算所产生的所有布尔值64=]
  4. 我们应该更喜欢空字符串 ('') 来表示无样式,而不是 None(为了一致的 dtype 处理)
  5. 与其绑定使用 python 字符串格式,我们应该使用(从 pandas 1.3.0 开始)Styler.to_html with doctype_html=True instead of Styler.render
    • 如果需要更复杂的 HTML 操作 Subclass should be used rather than attempting string manipulation on the results of render/to_html
# import numpy as np


def style_negative(v, props='color:red;'):
    return np.where(v < 0, props, '')


def style_red_date(v, props='color:red;'):
    return np.where(v < dt.datetime.now().date(), props, '')


styler = df.style.applymap(style_negative, subset=['Number'])
styler.apply(style_red_date, subset=['Date'])

mail_content = (
    styler
        .format({'Number': "{:.2f}"})
        .set_properties(**{'font-size': '11pt'})
        .hide_index()
        .to_html(doctype_html=True)
)

我们也可以做一个样式链,例如:

# Using same modified functions as above but as a function chain:
mail_content = df.style.applymap(
    style_negative, subset=['Number']
).apply(
    style_red_date, subset=['Date']
).format(
    {'Number': "{:.2f}"}
).set_properties(
    **{'font-size': '11pt'}
).hide_index().to_html(doctype_html=True)

这些选项在 mail_content 中产生以下 HTML:

<!DOCTYPE html>
<html>

<head>
  <meta charset="">
  <style type="text/css">
    #T_0f6b6_row0_col0,
    #T_0f6b6_row1_col1 {
      font-size: 11pt;
    }
    
    #T_0f6b6_row0_col1,
    #T_0f6b6_row1_col0 {
      color: red;
      font-size: 11pt;
    }
  </style>
</head>

<body>
  <table id="T_0f6b6_">
    <thead>
      <tr>
        <th class="col_heading level0 col0">Number</th>
        <th class="col_heading level0 col1">Date</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td id="T_0f6b6_row0_col0" class="data row0 col0">100.00</td>
        <td id="T_0f6b6_row0_col1" class="data row0 col1">2020-01-01</td>
      </tr>
      <tr>
        <td id="T_0f6b6_row1_col0" class="data row1 col0">-100.00</td>
        <td id="T_0f6b6_row1_col1" class="data row1 col1">2022-01-01</td>
      </tr>
    </tbody>
  </table>
</body>

</html>


设置:

import datetime as dt

import numpy as np
import pandas as pd

# Reproducible example with year offset so styles will always
# be reproducible same even in future years 
# (even though date values will change) 
df = pd.DataFrame({
    'Number': [100.00, -100.00],
    'Date': pd.to_datetime(
        [dt.date(dt.datetime.now().year - 1, 1, 1),
         dt.date(dt.datetime.now().year + 1, 1, 1)]
    ).date
})