将时间转换为指定的时间格式并应用于绘图的 y 轴

Convert times to designated time format and apply to y-axis of plotly graph

我目前正在尝试根据文章 An Interactive Web Dashboard with Plotly and Flask.

使用 plotly 和 flask 创建用于 Formula1 分析的网络仪表板

我有 MM:SS:sss 字符串格式的单圈时间(其中 MM 是分钟,sss 是毫秒)并且我尝试(下面的 python 脚本)使用 [= 将其转换为可量化的值12=] 这样我就可以绘制它们并对其进行操作(例如,找到驾驶员在多圈内的平均时间)。但是,当我尝试绘制 plotly 中的 timedelta 对象时,它们以微秒为单位显示。

是否有可能以指定的时间格式创建 timedelta 对象并对其进行量化,以便 plotly 正确绘制它们?

from datetime import timedelta

times = ["1:23.921", "1:24.690", "1:24.790"]

# convert to timedelta object
def string_con(string_time):
    new_time = timedelta(minutes=int(string_time.split(
        ":")[0]), seconds=int((string_time.split(":")[1]).split(".")[0]),
        milliseconds=int((string_time.split(":")[1]).split(".")[1]))
    return new_time


# compute average pace using timedelta objects
def average_pace(laps):
    laps = list(map(string_con, laps))
    return (sum(laps, timedelta(0))/len(laps))

print(average_pace(times))

您需要将 timedelta 的数字内部值转换为与其存储的时间单位不同的时间单位,即 days, seconds,以及microseconds.

既然你说你不能自己将它们转换成字符串,一个潜在的解决方法可能是将它们转换成一个 timedelta subclass ,它会按照你想要的方式将自己转换成一个字符串。

要记住的一件事是 timedelta 可能包含巨大的值,必须以某种方式处理这些值——所以只是说你想要 "MM:SS.sss" 格式会忽略事实上,理论上也可能涉及数天和数小时。下面的函数计算它们,但仅在它们非零时显示它们的值。

下面的代码定义了一个新的 MyTimeDelta subclass 并使用它。我已经将 subclass' __str__() 方法定义为 return 所需格式的字符串。每当新 class 的实例被转换为字符串时,都会使用这个以前的函数,但是 class 作为一个整体仍然是“数字的”,就像它的基础 class 一样。 subclass' __str__() 方法使用我还添加的私有辅助方法 _convert_units().

from datetime import timedelta

class MyTimeDelta(timedelta):
    @classmethod
    def from_another(cls, other):
        if not isinstance(other, timedelta):
            raise TypeError('unsupported type')
        return cls(days=other.days, seconds=other.seconds, microseconds=other.microseconds)

    def __str__(self):
        """ Format a timedelta into this format D:H:MM:SS.sss """
        res = []
        days, hours, minutes, seconds = self._convert_units()
        if days:
            res.append(f'{days}:')
        if hours or days:
            res.append(f'{hours}:')
        if minutes or hours or days:
            res.append(f'{minutes:02d}:')
        res.append(f'{seconds}')
        return ''.join(res)

    def _convert_units(self):
        """ Convert a timedelta to days, hours, minutes, & seconds."""
        days = self.days
        hours, remainder = divmod(self.seconds, 3600)
        minutes, seconds = divmod(remainder, 60)
        seconds += self.microseconds / 1e6
        return days, hours, minutes, seconds

times = ["1:23.921", "1:24.690", "1:24.790"]

def string_con(string_time):
    """ Convert string_time to timedelta object. """
    split_time = string_time.split(":")
    split_secs = split_time[1].split(".")
    mins, secs, ms = map(int, (split_time[0], split_secs[0], split_secs[1]))
    return timedelta(minutes=mins, seconds=secs, milliseconds=ms)

def average_pace(laps):
    """ Compute average pace using timedelta objects. """
    laps = [string_con(lap) for lap in laps]
    return sum(laps, timedelta(0)) / len(laps)


avg = MyTimeDelta.from_another(average_pace(times))
print(f'{avg.days=}, {avg.seconds=}, {avg.microseconds=}')
print(avg)

  • 您将文本转换为分析表示是正确的。我也使用了 Timedelta。在某些方面,使用纳秒会更简单
  • 您还需要转换回轴刻度和悬停文本。我为此使用了一个效用函数
  • 所有这些结合在一起,您可以创建 plotly 正确且人类可读的单圈时间图 ;-)
import requests
import pandas as pd
import plotly.express as px

# get some lap timing data
df = pd.concat([
        pd.json_normalize(requests.get(f"https://ergast.com/api/f1/2021/7/laps/{l}.json").json()
                          ["MRData"]["RaceTable"]["Races"][0]["Laps"][0]["Timings"]
        ).assign(lap=l)
        for l in range(1, 25)
    ]).reset_index(drop=True)
# convert to timedelta...
df["time"] = (
    df["time"]
    .str.extract(r"(?P<minute>[0-9]+):(?P<sec>[0-9]+).(?P<milli>[0-9]+)")
    .apply(
        lambda r: pd.Timestamp(year=1970,month=1,day=1,
                               minute=int(r.minute),second=int(r.sec),microsecond=int(r.milli) * 10 ** 3,
        ),
        axis=1,
    )
    - pd.to_datetime("1-jan-1970").replace(hour=0, minute=0, second=0, microsecond=0)
)

# utility build display string from nanoseconds
def strfdelta(t, fmt="{minutes:02d}:{seconds:02d}.{milli:03d}"):
    d = {}
    d["minutes"], rem = divmod(t, 10 ** 9 * 60)
    d["seconds"], d["milli"] = divmod(rem, 10 ** 9)
    d["milli"] = d["milli"] // 10**6
    return fmt.format(**d)

# build a figure with lap times data...  NB use of hover_name for formatted time
fig = px.scatter(
    df,
    x="lap",
    y="time",
    color="driverId",
    hover_name=df["time"].astype(int).apply(strfdelta),
    hover_data={"time":False},
    size=df.groupby("lap")["time"].transform(
        lambda s: s.rank(ascending=True).eq(1).astype(int)
    ),
)
# make figure more interesting... add best/worst and mean lap times...
fig.add_traces(
    px.line(
        df.groupby("lap")
        .agg(
            avg=("time", lambda s: s.mean()),
            min=("time", lambda s: s.min()),
            max=("time", lambda s: s.max()),
        )
        .reset_index(),
        x="lap",
        y=["avg", "min", "max"],
    ).data
)

# fix up tick labels
ticks = pd.Series(range(df["time"].astype(int).min() - 10 ** 10,df["time"].astype(int).max(),10 ** 10,))
fig.update_layout(
    yaxis={
        "range": [
            df["time"].astype(int).min() - 10 ** 10,
            df["time"].astype(int).max(),
        ],
        "tickmode": "array",
        "tickvals": ticks,
        "ticktext": ticks.apply(strfdelta)
    }
)