2010-12-07 38 views
3

J'ai un tableau des positions historiques des autobus à un moment donné, enregistré une fois par seconde. Le schéma ressemble à ceci:Calcul de la durée du temps SQL

BusID  int   not null, 
BreadcrumbID int   not null identity (1, 1), 
BusStopID int   null, 
Timestamp datetime not null 

Je veux générer un horaire d'arrêt de bus basé sur des voyages historiques. Un bus est "à l'arrêt" si son BusStopID correspond à l'arrêt, et n'est pas "à l'arrêt" si le BusStopID est nul.

J'ai besoin de générer les temps moyens du bus à chaque arrêt. Donc, fondamentalement, je dois faire ce qui suit:

  • Identifier les fois qu'un bus est à l'arrêt - d'une simple clause where le tour est joué
  • Identifier les temps moyens le bus est à l'arrêt. Pour mes fins, je définis un "temps d'arrêt" discret comme une fenêtre de plus/moins 10 minutes; Si un bus s'arrête un jour entre 10h04 et 10h08, un autre jour entre 10h06 et 10h08 et un troisième jour entre 10h14 et 10h18, ce sera le même arrêt, mais s'il s'arrête à 10h45 - 10h48, ce serait un arrêt différent.
  • Filtre à « bruit » - à savoir les arrêts temps qui ne se sont produits plusieurs fois mais jamais

Je suis tout à fait à une perte sur la façon d'atteindre les deuxième et troisième balle. S'il vous plaît aider!

+0

est 'Bus ID' vraiment censé incrémenter pour tous les horodateurs? Comme 'Timestamp' est en fait un type de données en SQL, je déconseille de l'utiliser comme un nom de colonne, mais je comprends que vous avez choisi un nom qui a du sens (contrairement au nom du type de données lui-même). – Brad

+0

Oups, j'ai fait une erreur en tapant le schéma dans StackOverflow. Vous avez raison, l'identifiant du fil d'Ariane augmente et le BusID est un FK. –

+1

Question intéressante, beaucoup plus délicate qu'il n'y paraît – smirkingman

Répondre

2

À un certain nombre d'occasions, j'ai fait quelque chose de similaire. Essentiellement, regroupement basé sur des séparations dans un ordre complexe. Les bases de l'approche que j'utilise, en ce qui concerne ce problème, sont les suivantes:

  1. Construire un tableau de toutes les plages de temps d'intérêt.
  2. Trouvez l'heure de début pour chaque groupe de plages d'intérêt.
  3. Trouvez l'heure de fin pour chaque groupe de plages d'intérêt.
  4. Joignez les heures de début et de fin à la liste des périodes et du groupe.

Ou, plus en détail: (chacune de ces étapes pourrait être une partie d'un grand CTE, mais je me suis cassé vers le bas dans les tables temporaires pour faciliter la lecture ...)

Étape 1 : Trouver la liste de toutes les plages d'intérêt (j'ai utilisé une méthode similaire à celle liée à @Brad). NOTE: comme l'a souligné Manfred Sorg, cela suppose qu'il n'y a pas de "secondes manquantes" dans les données d'un bus. S'il y a une interruption dans les horodatages, ce code interprétera la plage unique comme deux plages distinctes (ou plus).

;with stopSeconds as (
    select BusID, BusStopID, TimeStamp, 
     [date] = cast(datediff(dd,0,TimeStamp) as datetime), 
     [grp] = dateadd(ss, -row_number() over(partition by BusID order by TimeStamp), TimeStamp) 
    from #test 
    where BusStopID is not null 
) 
select BusID, BusStopID, date, 
     [sTime] = dateadd(ss,datediff(ss,date,min(TimeStamp)), 0), 
     [eTime] = dateadd(ss,datediff(ss,date,max(TimeStamp)), 0), 
     [secondsOfStop] = datediff(ss, min(TimeStamp), max(Timestamp)), 
     [sOrd] = row_number() over(partition by BusID, BusStopID order by datediff(ss,date,min(TimeStamp))), 
     [eOrd] = row_number() over(partition by BusID, BusStopID order by datediff(ss,date,max(TimeStamp))) 
into #ranges 
from stopSeconds 
group by BusID, BusStopID, date, grp 

Étape 2: Trouvez le temps plus tôt pour chaque arrêt

select this.BusID, this.BusStopID, this.sTime minSTime, 
     [stopOrder] = row_number() over(partition by this.BusID, this.BusStopID order by this.sTime) 
into #starts 
from #ranges this 
    left join #ranges prev on this.BusID = prev.BusID 
         and this.BusStopID = prev.BusStopID 
         and this.sOrd = prev.sOrd+1 
         and this.sTime between dateadd(mi,-10,prev.sTime) and dateadd(mi,10,prev.sTime) 
where prev.BusID is null 

Étape 3: Trouver la dernière fois pour chaque arrêt

select this.BusID, this.BusStopID, this.eTime maxETime, 
     [stopOrder] = row_number() over(partition by this.BusID, this.BusStopID order by this.eTime) 
into #ends 
from #ranges this 
    left join #ranges next on this.BusID = next.BusID 
         and this.BusStopID = next.BusStopID 
         and this.eOrd = next.eOrd-1 
         and this.eTime between dateadd(mi,-10,next.eTime) and dateadd(mi,10,next.eTime) 
where next.BusID is null 

Étape 4: Joignez-vous tout ensemble

select r.BusID, r.BusStopID, 
     [avgLengthOfStop] = avg(datediff(ss,r.sTime,r.eTime)), 
     [earliestStop] = min(r.sTime), 
     [latestDepart] = max(r.eTime) 
from #starts s 
    join #ends e on s.BusID=e.BusID 
       and s.BusStopID=e.BusStopID 
       and s.stopOrder=e.stopOrder 
    join #ranges r on r.BusID=s.BusID 
       and r.BusStopID=s.BusStopID 
       and r.sTime between s.minSTime and e.maxETime 
       and r.eTime between s.minSTime and e.maxETime 
group by r.BusID, r.BusStopID, s.stopOrder 
having count(distinct r.date) > 1 --filters out the "noise" 

Enfin, pour être complet, ranger:

drop table #ends 
drop table #starts 
drop table #ranges 
+0

Great! Un inconvénient: le regroupement échoue lorsque les données ne sont pas complètes - lorsqu'il y en a une seconde manquante pendant un arrêt de bus, vous obtenez deux arrêts de bus comptés. –

+0

Avec le 'avoir' final, ne produit qu'un seul résultat. Sans, produit des doublons et ne fait pas la moyenne des temps – smirkingman

+0

@Manfred, vous avez raison ... c'était une supposition que j'ai faite en fonction de l'ensemble de données que j'ai utilisé. Si cette hypothèse est invalide, vous pouvez faire quelque chose de similaire au reste du processus (c'est-à-dire trouver les début et fin de correspondance et le groupe). – chezy525

0

réponse ... Frais

Essayez ceci:

DECLARE @stopWindowMinutes INT 
SET @stopWindowMinutes = 10 

-- 
; 
WITH test_data 
      AS (SELECT 1 [BusStopId] 
         ,'2010-01-01 10:00:04' [Timestamp] 
       UNION SELECT 1,'2010-01-01 10:00:05' 
       UNION SELECT 1,'2010-01-01 10:00:06' 
       UNION SELECT 1,'2010-01-01 10:00:07' 
       UNION SELECT 1,'2010-01-01 10:00:08' 
       UNION SELECT 1,'2010-01-02 10:00:06' 
       UNION SELECT 1,'2010-01-02 10:00:07' 
       UNION SELECT 1,'2010-01-02 10:00:08' 
       UNION SELECT 2,'2010-01-01 10:00:06' 
       UNION SELECT 2,'2010-01-01 10:00:07' 
       UNION SELECT 2,'2010-01-01 10:00:08' 
       UNION SELECT 2,'2010-01-01 10:00:09' 
       UNION SELECT 2,'2010-01-01 10:00:10' 
       UNION SELECT 2,'2010-01-01 10:00:09' 
       UNION SELECT 2,'2010-01-01 10:00:10' 
       UNION SELECT 2,'2010-01-01 10:00:11' 
       UNION SELECT 1,'2010-01-02 10:33:43' 
       UNION SELECT 1,'2010-01-02 10:33:44' 
       UNION SELECT 1,'2010-01-02 10:33:45' 
       UNION SELECT 1,'2010-01-02 10:33:46' 
      ) 
    SELECT DISTINCT 
      [BusStopId] 
      ,[AvgStop] 
    FROM (SELECT [a].[BusStopId] 
         ,(SELECT MIN([b].[Timestamp]) 
          FROM  [test_data] b 
          WHERE  [a].[BusStopId] = [b].[BusStopId] 
            AND CONVERT(VARCHAR(10), [a].[Timestamp], 120) = CONVERT(VARCHAR(10), [b].[Timestamp], 120) 
            AND [b].[Timestamp] BETWEEN DATEADD(SECOND, [email protected] * 60, 
                     [a].[Timestamp]) 
                 AND  DATEADD(SECOND, @stopWindowMinutes * 60, [a].[Timestamp]) -- w/i X minutes 

         ) [MinStop] 
         ,(SELECT MAX([b].[Timestamp]) 
          FROM  [test_data] b 
          WHERE  [a].[BusStopId] = [b].[BusStopId] 
            AND CONVERT(VARCHAR(10), [a].[Timestamp], 120) = CONVERT(VARCHAR(10), [b].[Timestamp], 120) 
            AND [b].[Timestamp] BETWEEN DATEADD(SECOND, [email protected] * 60, 
                     [a].[Timestamp]) 
                 AND  DATEADD(SECOND, @stopWindowMinutes * 60, [a].[Timestamp]) -- w/i X minutes 

         ) [MaxStop] 
         ,(SELECT DATEADD(second, 
              AVG(DATEDIFF(second, CONVERT(VARCHAR(10), [b].[Timestamp], 120), 
                 [b].[Timestamp])), 
              CONVERT(VARCHAR(10), MIN([b].[Timestamp]), 120)) 
          FROM  [test_data] b 
          WHERE  [a].[BusStopId] = [b].[BusStopId] 
            AND CONVERT(VARCHAR(10), [a].[Timestamp], 120) = CONVERT(VARCHAR(10), [b].[Timestamp], 120) 
            AND [b].[Timestamp] BETWEEN DATEADD(SECOND, [email protected] * 60, 
                     [a].[Timestamp]) 
                 AND  DATEADD(SECOND, @stopWindowMinutes * 60, [a].[Timestamp]) -- w/i X minutes 

         ) [AvgStop] 
       FROM  [test_data] a 
       WHERE  CONVERT(VARCHAR(10), [Timestamp], 120) = CONVERT(VARCHAR(10), [Timestamp], 120) 
       GROUP BY [a].[BusStopId] 
         ,[a].[Timestamp] 
      ) subset1 
+0

Je pense que vous avez mal lu - les arrêts de bus sont identifiés par le BusStopID; l'arrêt de bus * fois * sont identifiés par les temps. –

+0

@David, j'ai mis à jour ma réponse; voyez si cela vous mène dans la bonne direction. Je n'ai pas fait de POC, mais l'idée * semble * solide. – Brad

+0

@David, j'ai ajouté un POC à ma réponse. Voyez si c'est ce que vous voulez. – Brad

2

This post Je viens de voir peut vous aider. (Sql Server Central)

0

Comme est souvent le cas, ce genre de problèmes sont plus faciles à résoudre et maintenir en les cassant en morceaux bouchées:

-- Split into Date and minutes-since-midnight 
WITH observed(dates,arrival,busstop,bus) AS (
    SELECT 
     CONVERT(CHAR(8), TimeStamp, 112), 
     DATEPART(HOUR,TimeStamp) * 60 + DATEPART(MINUTE,TimeStamp), 
     busstopid, 
     busid 
    FROM 
     History 
), 
-- Identify times at stop subsequent to arrival at that stop 
atstop(dates,stoptime,busstop,bus) AS (
    SELECT 
     a.dates, 
     a.arrival, 
     a.busstop, 
     a.bus 
    FROM 
     observed a 
    WHERE 
     EXISTS (
      SELECT 
       * 
      FROM 
       observed b 
      WHERE 
       a.dates = b.dates AND 
       a.busstop = b.busstop AND 
       a.bus = b.bus AND 
       a.arrival - b.arrival BETWEEN 1 AND 10 
     ) 
), 
-- Isolate actual arrivals at stops, excluding waiting at stops 
dailyhalts(dates,arrival,busstop,bus) AS (
    SELECT 
     a.dates,a.arrival,a.busstop,a.bus 
    FROM 
     observed a 
    WHERE 
     arrival NOT IN (
      SELECT 
       stoptime 
      FROM 
       atstop b 
      WHERE 
       a.dates = b.dates AND 
       a.busstop = b.busstop AND 
       a.bus = b.bus 
    ) 
), 
-- Merge arrivals across all dates 
timetable(busstop,bus,arrival) AS (
    SELECT 
     a.busstop, a.bus, a.arrival 
    FROM 
     dailyhalts a 
    WHERE 
     NOT EXISTS (
      SELECT 
       * 
      FROM 
       dailyhalts h 
      WHERE 
       a.busstop = h.busstop AND 
       a.bus = h.bus AND 
       a.arrival - h.arrival BETWEEN 1 AND 10 
     ) 
    GROUP BY 
     a.busstop, a.bus, a.arrival 
) 
-- Print timetable for a given day 
SELECT 
    a.busstop, a.bus, a.arrival, DATEADD(minute,AVG(b.arrival),'2010/01/01') 
FROM 
    timetable a INNER JOIN 
    observed b ON 
     a.busstop = b.busstop AND 
     a.bus = b.bus AND 
     b.arrival BETWEEN a.arrival AND a.arrival + 10 
GROUP BY 
    a.busstop, a.bus, a.arrival 

entrée :

ID BusID BusStopID TimeStamp 
1 1 1 2010-01-01 10:00:00.000 
2 1 1 2010-01-01 10:01:00.000 
3 1 1 2010-01-01 10:02:00.000 
4 1 2 2010-01-01 11:00:00.000 
5 1 3 2010-01-01 12:00:00.000 
6 1 3 2010-01-01 12:01:00.000 
7 1 3 2010-01-01 12:02:00.000 
8 1 3 2010-01-01 12:03:00.000 
9 1 1 2010-01-02 11:00:00.000 
10 1 1 2010-01-02 11:03:00.000 
11 1 1 2010-01-02 11:07:00.000 
12 1 2 2010-01-02 12:00:00.000 
13 1 3 2010-01-02 13:00:00.000 
14 1 3 2010-01-02 13:01:00.000 
15 1 1 2010-01-03 10:03:00.000 
16 1 1 2010-01-03 10:05:00.000 

sortie:

busstop bus arrival (No column name) 
1 1 600 2010-01-01 10:02:00.000 
1 1 660 2010-01-01 11:03:00.000 
2 1 660 2010-01-01 11:00:00.000 
2 1 720 2010-01-01 12:00:00.000 
3 1 720 2010-01-01 12:01:00.000 
3 1 780 2010-01-01 13:00:00.000